├── .husky
├── .gitignore
├── pre-commit
└── commit-msg
├── src
├── components
│ ├── home
│ │ ├── index.ts
│ │ └── background
│ │ │ ├── CrossPattern.tsx
│ │ │ └── index.tsx
│ ├── index.ts
│ └── common
│ │ ├── UserAgent
│ │ └── index.ts
│ │ ├── Providers
│ │ └── index.tsx
│ │ └── Image
│ │ ├── image.stories.tsx
│ │ └── index.tsx
├── config
│ ├── index.ts
│ └── seo.ts
├── globals.d.ts
├── hooks
│ ├── index.ts
│ ├── useIsMobile.ts
│ └── useDetectKeyboard.ts
├── lib
│ ├── index.ts
│ ├── test-utils.ts
│ ├── modifyResponsiveValue.ts
│ ├── modifyResponsiveValue.test.ts
│ ├── gtag.ts
│ └── sentry.ts
├── pages
│ ├── 404.tsx
│ ├── api
│ │ ├── passwordCheck.ts
│ │ ├── login.ts
│ │ └── sitemap.ts
│ ├── index.tsx
│ ├── _error.tsx
│ ├── _document.tsx
│ └── _app.tsx
├── styles
│ ├── styled.d.ts
│ ├── theme
│ │ ├── space.ts
│ │ ├── colors.ts
│ │ └── index.ts
│ └── CSSreset.ts
├── __tests__
│ └── pages
│ │ └── api
│ │ └── sitemap.test.ts
└── docs
│ └── introduction.stories.mdx
├── setup-jest.ts
├── cypress.json
├── public
├── favicon.ico
└── static
│ └── fonts
│ ├── Inter-Bold.woff
│ ├── Inter-Bold.woff2
│ ├── Inter-Italic.woff
│ ├── Inter-Medium.woff
│ ├── Inter-Italic.woff2
│ ├── Inter-Medium.woff2
│ ├── Inter-Regular.woff
│ ├── Inter-Regular.woff2
│ ├── DomaineDisp-Bold.woff
│ ├── DomaineDisp-Bold.woff2
│ ├── Inter-BoldItalic.woff
│ ├── Inter-BoldItalic.woff2
│ ├── Inter-MediumItalic.woff
│ ├── Inter-MediumItalic.woff2
│ └── stylesheet.css
├── cypress
├── support
│ ├── commands.ts
│ └── index.ts
├── tsconfig.json
├── fixtures
│ └── example.json
├── .eslintrc.js
├── e2e
│ └── home.spec.ts
└── plugins
│ └── index.ts
├── .prettierignore
├── .eslintignore
├── .storybook
├── manager.ts
├── ProviderDecorator.tsx
├── main.js
├── viewports.ts
├── theme.ts
├── preview.tsx
└── webpack.config.js
├── next-env.d.ts
├── .babelrc
├── .gitlab-ci.yml
├── .gitignore
├── jest.config.ts
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── workflows
│ ├── release.yaml
│ └── test.yaml
└── CONTRIBUTING.md
├── .releaserc.json
├── tsconfig.json
├── LICENSE
├── .vscode
└── settings.json
├── .cz-config.js
├── next.config.js
├── package.json
└── README.md
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/src/components/home/index.ts:
--------------------------------------------------------------------------------
1 | export * from './background';
2 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export { default as seo } from './seo';
2 |
--------------------------------------------------------------------------------
/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useIsMobile } from './useIsMobile';
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn lint-staged
5 |
--------------------------------------------------------------------------------
/setup-jest.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import 'jest-axe/extend-expect';
3 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:3000",
3 | "viewport": "macbook-15"
4 | }
5 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | import 'cypress-hmr-restarter';
2 | import '@testing-library/cypress/add-commands';
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | public/*
3 | .next
4 | .next/*
5 | dist
6 | coverage
7 | coverage-ts
8 | storybook-static/*
9 |
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Bold.woff
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules/**
2 | /public/**
3 | /storybook-static/**
4 | /dist/**
5 | /.next/**
6 | /coverage/**
7 | /coverage-ts/**
8 |
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Bold.woff2
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Italic.woff
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Medium.woff
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Italic.woff2
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Medium.woff2
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Regular.woff
--------------------------------------------------------------------------------
/public/static/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './common/Image';
2 | export * from './common/Providers';
3 | export * from './common/UserAgent';
4 |
--------------------------------------------------------------------------------
/public/static/fonts/DomaineDisp-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/DomaineDisp-Bold.woff
--------------------------------------------------------------------------------
/public/static/fonts/DomaineDisp-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/DomaineDisp-Bold.woff2
--------------------------------------------------------------------------------
/public/static/fonts/Inter-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-BoldItalic.woff
--------------------------------------------------------------------------------
/public/static/fonts/Inter-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-BoldItalic.woff2
--------------------------------------------------------------------------------
/.storybook/manager.ts:
--------------------------------------------------------------------------------
1 | import {addons} from '@storybook/addons'
2 |
3 | import theme from './theme'
4 |
5 | addons.setConfig({
6 | theme,
7 | })
8 |
--------------------------------------------------------------------------------
/public/static/fonts/Inter-MediumItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-MediumItalic.woff
--------------------------------------------------------------------------------
/public/static/fonts/Inter-MediumItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyofams/next-boilerplate/HEAD/public/static/fonts/Inter-MediumItalic.woff2
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './test-utils';
2 | export * from './sentry';
3 | export * from './gtag';
4 | export * from './modifyResponsiveValue';
5 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import Error from './_error';
2 |
3 | const NotFoundPage = () => ;
4 |
5 | export default NotFoundPage;
6 |
--------------------------------------------------------------------------------
/src/pages/api/passwordCheck.ts:
--------------------------------------------------------------------------------
1 | import { passwordCheckHandler } from '@storyofams/next-password-protect';
2 |
3 | export default passwordCheckHandler(process.env.STAGING_PASSWORD);
4 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["es5", "dom"],
5 | "types": ["cypress", "node"]
6 | },
7 | "include": ["**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/api/login.ts:
--------------------------------------------------------------------------------
1 | import { loginHandler } from '@storyofams/next-password-protect';
2 |
3 | export default loginHandler(process.env.STAGING_PASSWORD, {
4 | cookieSameSite: 'none',
5 | });
6 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | plugins: ['eslint-plugin-cypress'],
4 | extends: ['@storyofams/eslint-config-ams/web', 'plugin:cypress/recommended'],
5 | env: { 'cypress/globals': true },
6 | };
7 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | "babel-plugin-transform-typescript-metadata",
5 | ["@babel/plugin-proposal-decorators", { "legacy": true }],
6 | "babel-plugin-parameter-decorator",
7 | ["styled-components", {"ssr": true}]
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: node:latest
2 |
3 | cache:
4 | paths:
5 | - node_modules/
6 |
7 | stages:
8 | - test
9 |
10 | test:
11 | before_script:
12 | - yarn config set cache-folder .yarn
13 | - yarn install
14 | stage: test
15 | script: yarn jest --coverage
16 | only:
17 | - merge_requests
18 |
--------------------------------------------------------------------------------
/.storybook/ProviderDecorator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Providers } from "../src/components";
4 | import CSSreset from "../src/styles/CSSreset";
5 |
6 | const ProviderDecorator = (storyFn) => (
7 |
8 |
9 | {storyFn()}
10 |
11 | );
12 |
13 | export default ProviderDecorator;
14 |
--------------------------------------------------------------------------------
/src/lib/test-utils.ts:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { Providers } from '~components';
3 |
4 | const customRender = (ui, options?) =>
5 | render(ui, { wrapper: Providers, ...options });
6 |
7 | // re-export everything
8 | export * from '@testing-library/react';
9 |
10 | // override render method
11 | export { customRender as render };
12 |
--------------------------------------------------------------------------------
/src/lib/modifyResponsiveValue.ts:
--------------------------------------------------------------------------------
1 | export const modifyResponsiveValue = (value, cb) => {
2 | if (typeof value === 'string' || typeof value === 'number') {
3 | return cb(value);
4 | } else if (Array.isArray(value)) {
5 | return value.map(cb);
6 | } else {
7 | const newObj = {};
8 | Object.keys(value).forEach((k) => (newObj[k] = cb(value[k])));
9 | return newObj;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/styles/styled.d.ts:
--------------------------------------------------------------------------------
1 | export type Breakpoints = {
2 | sm?: string;
3 | md?: string;
4 | lg?: string;
5 | xl?: string;
6 | };
7 |
8 | type StyledTheme = typeof import('./theme').default;
9 |
10 | declare module 'styled-components' {
11 | export interface DefaultTheme extends StyledTheme {}
12 | }
13 |
14 | declare module 'system-props' {
15 | export interface Theme extends StyledTheme {}
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/modifyResponsiveValue.test.ts:
--------------------------------------------------------------------------------
1 | import { modifyResponsiveValue } from './modifyResponsiveValue';
2 |
3 | test('works for all kind of responsive data structures', () => {
4 | expect(modifyResponsiveValue([1, 2, 3], (n) => n + 1)).toEqual([2, 3, 4]);
5 | expect(modifyResponsiveValue({ a: 1, b: 2 }, (n) => n - 1)).toEqual({
6 | a: 0,
7 | b: 1,
8 | });
9 | expect(modifyResponsiveValue(12, (n) => n * 2)).toEqual(24);
10 | });
11 |
--------------------------------------------------------------------------------
/src/styles/theme/space.ts:
--------------------------------------------------------------------------------
1 | const space = {
2 | 0: 0,
3 | 0.25: 2,
4 | 0.5: 4,
5 | 0.75: 6,
6 | 1: 8,
7 | 1.25: 10,
8 | 1.5: 12,
9 | 1.75: 14,
10 | 2: 16,
11 | 2.25: 18,
12 | 2.5: 20,
13 | 3: 24,
14 | 4: 32,
15 | 5: 40,
16 | 6: 48,
17 | 7: 56,
18 | 8: 64,
19 | 9: 72,
20 | 10: 80,
21 | 12: 96,
22 | 15: 120,
23 | 16: 128,
24 | 18: 144,
25 | 20: 160,
26 | } as const;
27 |
28 | export default space;
29 |
--------------------------------------------------------------------------------
/src/lib/gtag.ts:
--------------------------------------------------------------------------------
1 | declare interface GtagWindow extends Window {
2 | gtag: any;
3 | }
4 | declare const window: GtagWindow;
5 |
6 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
7 | export const pageview = (url: string) => {
8 | window.gtag('config', process.env.NEXT_PUBLIC_GTM, {
9 | page_path: url,
10 | });
11 | };
12 |
13 | export const track = (data: any) => {
14 | try {
15 | window.gtag(data);
16 | } catch (e) {}
17 | };
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 | /coverage-ts
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | .env*
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # storybook
29 | storybook-static
30 |
31 | # cypress
32 | cypress/videos
33 | cypress/screenshots
34 |
--------------------------------------------------------------------------------
/src/components/common/UserAgent/index.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | /**
4 | * Detect user agent and add `data-ios` attribute to document if iOS.
5 | */
6 | export const UserAgent = () => {
7 | useEffect(() => {
8 | // Browser only
9 | if (typeof window !== 'undefined') {
10 | const ua = navigator?.userAgent?.toString();
11 | if (/(iPhone|iPad)/.test(ua)) {
12 | document.documentElement.setAttribute('data-ios', '');
13 | }
14 | }
15 | }, []);
16 |
17 | return null;
18 | };
19 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | refs: {
3 | ["react-ui"]: {
4 | title: "@storyofams/react-ui",
5 | url: "https://react-ui.storyofams.vercel.app/"
6 | }
7 | },
8 | stories: ['../src/docs/*.stories.@(mdx)', '../src/**/*.stories.tsx'],
9 | addons: [
10 | {
11 | name: '@storybook/addon-docs',
12 | options: {
13 | configureJSX: true,
14 | },
15 | },
16 | '@storybook/addon-actions',
17 | '@storybook/addon-controls',
18 | '@storybook/addon-viewport',
19 | ],
20 | };
21 |
--------------------------------------------------------------------------------
/src/config/seo.ts:
--------------------------------------------------------------------------------
1 | const siteTitle = 'Boilerplate';
2 |
3 | const defaultSeo = {
4 | openGraph: {
5 | type: 'website',
6 | locale: 'en_IE',
7 | url: 'https://www.Boilerplate.com/',
8 | site_name: siteTitle,
9 | },
10 | twitter: {
11 | handle: '@Boilerplate',
12 | cardType: 'summary_large_image',
13 | },
14 | titleTemplate: `%s - ${siteTitle}`,
15 | };
16 |
17 | if (process.env.NODE_ENV === 'development') {
18 | defaultSeo.titleTemplate = `DEV: %s - ${siteTitle}`;
19 | }
20 |
21 | export default defaultSeo;
22 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types';
2 |
3 | export default {
4 | testEnvironment: 'jsdom',
5 | roots: ['/src'],
6 | transform: {
7 | '^.+\\.tsx?$': 'ts-jest',
8 | },
9 | moduleNameMapper: {
10 | '^~(.*)$': '/src/$1',
11 | },
12 | moduleDirectories: [
13 | 'node_modules',
14 | 'src/lib', // a utility folder
15 | __dirname, // the root directory
16 | ],
17 | setupFilesAfterEnv: ['./setup-jest.ts'],
18 | coverageDirectory: './coverage',
19 | } as Config.InitialOptions;
20 |
--------------------------------------------------------------------------------
/cypress/e2e/home.spec.ts:
--------------------------------------------------------------------------------
1 | beforeEach(() => {
2 | cy.visit('/');
3 | });
4 |
5 | describe('Home page general', () => {
6 | it('should have valid seo tags', () => {
7 | cy.title().should('contain', 'Home -');
8 | });
9 | });
10 |
11 | describe('Home page mobile', () => {
12 | beforeEach(() => {
13 | cy.viewport('iphone-5');
14 | });
15 |
16 | it('should render the page', () => {});
17 | });
18 |
19 | describe('Home page desktop', () => {
20 | beforeEach(() => {
21 | cy.viewport('macbook-15');
22 | });
23 |
24 | it('should render the page', () => {});
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/home/background/CrossPattern.tsx:
--------------------------------------------------------------------------------
1 | export const CrossPattern = () => (
2 |
3 |
9 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Heading, Box } from '@storyofams/react-ui';
2 | import { NextSeo } from 'next-seo';
3 | import { Background } from '~components/home';
4 |
5 | const Home = () => (
6 | <>
7 |
8 |
9 |
10 |
16 | What will your Story be?
17 |
18 |
19 | >
20 | );
21 |
22 | export default Home;
23 |
--------------------------------------------------------------------------------
/cypress/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | import './commands';
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: Bug report
4 | about: Create a report to help us improve
5 | title: ''
6 | labels: bug
7 | assignees: ''
8 | ---**Describe the bug**
9 | A clear and concise description of what the bug is.
10 |
11 | **To Reproduce**
12 | Steps to reproduce the behavior:
13 |
14 | 1. Go to '...'
15 | 2. Click on '....'
16 | 3. Scroll down to '....'
17 | 4. See error
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots or codesandbox**
23 | If applicable, add screenshots to help explain your problem.
24 |
25 | **Additional context**
26 | Add any other context about the problem here.
27 |
--------------------------------------------------------------------------------
/.storybook/viewports.ts:
--------------------------------------------------------------------------------
1 | import {INITIAL_VIEWPORTS} from '@storybook/addon-viewport'
2 | import theme from '../src/styles/theme'
3 |
4 | const viewportByBreakpoint = () => {
5 | const viewPorts = {}
6 |
7 | theme.breakpoints.forEach((breakpoint, key) =>
8 | Object.assign(viewPorts, {
9 | [key]: {
10 | name: `Breakpoint: ${breakpoint}`,
11 | styles: {
12 | width: breakpoint,
13 | height: '963px',
14 | },
15 | },
16 | }),
17 | )
18 |
19 | return viewPorts
20 | }
21 |
22 | const customViewPorts = {
23 | ...viewportByBreakpoint(),
24 | }
25 |
26 | export const viewPorts = {
27 | ...customViewPorts,
28 | ...INITIAL_VIEWPORTS,
29 | }
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | name: Feature request
4 | about: Suggest an idea for this project
5 | title: ''
6 | labels: enhancement
7 | assignees: ''
8 | ---**Is your feature request related to a problem? Please describe.**
9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
10 |
11 | **Describe the solution you'd like**
12 | A clear and concise description of what you want to happen.
13 |
14 | **Describe alternatives you've considered**
15 | A clear and concise description of any alternative solutions or features you've considered.
16 |
17 | **Additional context**
18 | Add any other context or screenshots about the feature request here.
19 |
--------------------------------------------------------------------------------
/src/hooks/useIsMobile.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useContext } from 'react';
2 | import { ThemeContext } from 'styled-components';
3 |
4 | const useIsMobile = () => {
5 | const theme = useContext(ThemeContext);
6 | const [isMobile, setIsMobile] = useState(false);
7 |
8 | useEffect(() => {
9 | const checkIfMobile = () =>
10 | setIsMobile(
11 | window.innerWidth < Number(theme.breakpoints.md.replace(/\D/g, '')),
12 | );
13 |
14 | checkIfMobile();
15 |
16 | window.addEventListener('resize', checkIfMobile);
17 | return () => window.removeEventListener('resize', checkIfMobile);
18 | }, [theme.breakpoints.md]);
19 |
20 | return isMobile;
21 | };
22 |
23 | export default useIsMobile;
24 |
--------------------------------------------------------------------------------
/src/components/common/Providers/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from 'react';
2 | import { QueryClient, QueryClientProvider } from 'react-query';
3 | import { Hydrate } from 'react-query/hydration';
4 | import { ThemeProvider } from 'styled-components';
5 |
6 | import theme from '~styles/theme';
7 |
8 | type ProviderProps = {
9 | pageProps: any;
10 | children: ReactNode;
11 | };
12 |
13 | // All global context providers
14 | export const Providers = ({ pageProps, children }: ProviderProps) => {
15 | const [queryClient] = useState(() => new QueryClient());
16 |
17 | return (
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "@semantic-release/commit-analyzer",
5 | {
6 | "preset": "angular",
7 | "releaseRules": [
8 | {
9 | "release": "patch",
10 | "type": "chore"
11 | },
12 | {
13 | "release": "patch",
14 | "type": "refactor"
15 | },
16 | {
17 | "release": "patch",
18 | "type": "style"
19 | }
20 | ]
21 | }
22 | ],
23 | "@semantic-release/release-notes-generator",
24 | [
25 | "@semantic-release/changelog",
26 | {
27 | "changelogFile": "CHANGELOG.md"
28 | }
29 | ],
30 | [
31 | "@semantic-release/github",
32 | {
33 | "assets": ["CHANGELOG.md"]
34 | }
35 | ]
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/.storybook/theme.ts:
--------------------------------------------------------------------------------
1 | import {create} from '@storybook/theming/create'
2 | import theme from '../src/styles/theme'
3 |
4 | // import _LOGO from '../public/static/ams.png'
5 |
6 | export default create({
7 | base: 'light',
8 |
9 | colorPrimary: theme.colors.primary500,
10 | colorSecondary: theme.colors.secondary500,
11 |
12 | // UI
13 | appBg: theme.colors.white,
14 | appBorderColor: theme.colors.grey200,
15 |
16 | // Typography
17 | // fontBase: theme.fonts.body,
18 | // fontCode: theme.fonts.mono,
19 |
20 | // Text colors
21 | textColor: theme.colors.grey800,
22 |
23 | // Toolbar default and active colors
24 | barTextColor: theme.colors.grey700,
25 |
26 | inputTextColor: theme.colors.grey800,
27 |
28 | brandTitle: 'Story of AMS | Next Boilerplate',
29 | brandUrl: 'https://github.com/storyofams/next-boilerplates',
30 | // brandImage: _LOGO,
31 | })
32 |
--------------------------------------------------------------------------------
/src/components/home/background/index.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, css } from '@storyofams/react-ui';
2 |
3 | import { CrossPattern } from './CrossPattern';
4 |
5 | export const Background = () => (
6 |
33 |
34 |
35 | );
36 |
--------------------------------------------------------------------------------
/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import {DocsPage, DocsContainer} from '@storybook/addon-docs/blocks'
2 |
3 | import ProviderDecorator from "./ProviderDecorator";
4 | import { viewPorts } from './viewports';
5 | import * as nextImage from 'next/image';
6 | import "../public/static/fonts/stylesheet.css";
7 | import { Box } from '@storyofams/react-ui';
8 |
9 | export const decorators = [ProviderDecorator]
10 |
11 | export const parameters = {
12 | viewPort: {
13 | viewports: viewPorts
14 | },
15 | docs: {
16 | container: DocsContainer,
17 | page: DocsPage,
18 | },
19 | options: {
20 | storySort: {
21 | method: 'alphabetical',
22 | order: ['intro', 'components'],
23 | },
24 | },
25 | };
26 |
27 |
28 | Object.defineProperty(nextImage, 'default', {
29 | configurable: true,
30 | value: (props) => {
31 | return ;
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/cypress/plugins/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | Object.assign(config, {
22 | integrationFolder: 'cypress/e2e',
23 | });
24 |
25 | return config;
26 | };
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "typeRoots": ["./node_modules/@types"],
6 | "types": ["jest", "node"],
7 | "allowJs": true,
8 | "skipLibCheck": true,
9 | "strict": false,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "emitDecoratorMetadata": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "baseUrl": "./",
20 | "sourceMap": true,
21 | "inlineSources": true,
22 | "sourceRoot": "/",
23 | "experimentalDecorators": true,
24 | "paths": {
25 | "~*": ["src/*"],
26 | "test-utils": ["src/lib/test-utils"]
27 | }
28 | },
29 | "exclude": ["node_modules"],
30 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
31 | }
32 |
--------------------------------------------------------------------------------
/src/pages/api/sitemap.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createHandler,
3 | Get,
4 | Header,
5 | SetHeader,
6 | } from '@storyofams/next-api-decorators';
7 | import { SitemapStream } from 'sitemap';
8 |
9 | export class Sitemap {
10 | private async fetchMySitemapData() {
11 | const res = (await fetch('/myapi').then((res) => res.json())) as any[];
12 | return res.map((item) => ({
13 | lastmod: new Date(item.publishedAt).toISOString(),
14 | img: item.imageSrc,
15 | url: item.href,
16 | }));
17 | }
18 |
19 | @Get()
20 | @SetHeader('Content-Type', 'text/xml')
21 | @SetHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
22 | public async sitemap(@Header('host') host: string) {
23 | const data = await this.fetchMySitemapData();
24 | const smStream = new SitemapStream({
25 | hostname: 'https://' + host,
26 | });
27 |
28 | for (const smItem of data) {
29 | smStream.write(smItem);
30 | }
31 |
32 | smStream.end();
33 |
34 | return smStream;
35 | }
36 | }
37 |
38 | export default createHandler(Sitemap);
39 |
--------------------------------------------------------------------------------
/src/hooks/useDetectKeyboard.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | const useDetectKeyboard = (className?: string): boolean => {
4 | const [isTabbed, setIsTabbed] = useState(false);
5 |
6 | useEffect(() => {
7 | function handleFirstTab(e: KeyboardEvent) {
8 | if (e.keyCode === 9) {
9 | setIsTabbed(true);
10 | document.body.classList.add(className || 'user-is-tabbing');
11 | window.removeEventListener('keydown', handleFirstTab);
12 | }
13 | }
14 | function handleMouseMove() {
15 | setIsTabbed(false);
16 | document.body.classList.remove(className || 'user-is-tabbing');
17 | window.removeEventListener('mousemove', handleMouseMove);
18 | }
19 |
20 | window.addEventListener('keydown', handleFirstTab);
21 | window.addEventListener('mousemove', handleMouseMove);
22 | return () => {
23 | window.removeEventListener('keydown', handleFirstTab);
24 | window.addEventListener('mousemove', handleMouseMove);
25 | };
26 | }, [className]);
27 |
28 | return isTabbed;
29 | };
30 |
31 | export default useDetectKeyboard;
32 |
--------------------------------------------------------------------------------
/src/lib/sentry.ts:
--------------------------------------------------------------------------------
1 | import { RewriteFrames } from '@sentry/integrations';
2 | import * as Sentry from '@sentry/node';
3 |
4 | export const initSentry = () => {
5 | const integrations = [];
6 | if (
7 | process.env.NEXT_IS_SERVER === 'true' &&
8 | process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR
9 | ) {
10 | // For Node.js, rewrite Error.stack to use relative paths, so that source
11 | // maps starting with ~_next map to files in Error.stack with path
12 | // app:///_next
13 | integrations.push(
14 | new RewriteFrames({
15 | iteratee: (frame) => {
16 | frame.filename = frame.filename.replace(
17 | process.env.NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR,
18 | 'app:///',
19 | );
20 | frame.filename = frame.filename.replace('.next', '_next');
21 | return frame;
22 | },
23 | }),
24 | );
25 | }
26 |
27 | Sentry.init({
28 | enabled: process.env.NODE_ENV === 'production',
29 | integrations,
30 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
31 | release: process.env.NEXT_PUBLIC_COMMIT_SHA,
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-18.04
10 | steps:
11 | - name: Checkout repo
12 | uses: actions/checkout@v2
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: 12
20 |
21 | - name: Get yarn cache directory path
22 | id: yarn-cache-dir-path
23 | run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
24 |
25 | - name: Cache yarn dependencies
26 | uses: actions/cache@v2
27 | with:
28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
30 | restore-keys: |
31 | ${{ runner.os }}-yarn-
32 |
33 | - name: Install dependencies
34 | run: yarn
35 |
36 | - name: Release
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 | run: yarn semantic-release
40 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack")
2 | const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
3 |
4 | module.exports = ({ config }) => {
5 | config.resolve.plugins = [new TsconfigPathsPlugin({ extensions: config.resolve.extensions })]
6 |
7 | config.module.rules.push({
8 | test: /\.(ts|tsx)$/,
9 | loader: require.resolve("babel-loader"),
10 | options: {
11 | presets: [["react-app", { flow: false, typescript: true }]],
12 | },
13 | });
14 |
15 | config.resolve.extensions.push(".ts", ".tsx");
16 |
17 | config.module.rules.push({
18 | test: /\.svg$/,
19 | use: [{ loader: "@svgr/webpack", options: { icon: true, svgo: true } }],
20 | });
21 |
22 | config.plugins.push(new webpack.DefinePlugin({
23 | 'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({
24 | deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
25 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
26 | domains: ['placekitten.com', 'res.cloudinary.com'],
27 | path: '/',
28 | loader: 'default',
29 | }),
30 | }));
31 |
32 | return config;
33 | };
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Story of AMS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.watcherExclude": {
3 | "**/.cache/**": true,
4 | "storybook-static": true
5 | },
6 | "files.exclude": {
7 | "**/coverage": true,
8 | "**/coverage-ts": true,
9 | "**/node_modules": true,
10 | "**/.next": true,
11 | "**/storybook-static": true,
12 | "**/yarn-error.log": true,
13 | "**/next-env.d.ts": true,
14 | "**/yarn.lock": true,
15 | ".babelrc": true,
16 | ".cz-config.js": true,
17 | ".eslintignore": true,
18 | ".gitignore": true,
19 | ".prettierignore": true,
20 | ".releaserc.json": true,
21 | },
22 | "search.exclude": {
23 | "**/.next": true,
24 | "storybook-static": true
25 | },
26 | "editor.formatOnSave": false,
27 | "[javascript]": {
28 | "editor.formatOnSave": false
29 | },
30 | "[javascriptreact]": {
31 | "editor.formatOnSave": false
32 | },
33 | "[typescript]": {
34 | "editor.formatOnSave": false
35 | },
36 | "[typescriptreact]": {
37 | "editor.formatOnSave": false
38 | },
39 | "editor.codeActionsOnSave": {
40 | "source.fixAll": true
41 | },
42 | "prettier.disableLanguages": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/common/Image/image.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from '@storyofams/react-ui';
2 | import { Box } from '@storyofams/react-ui';
3 |
4 | import { Image, ImageProps } from '~components';
5 |
6 | export default {
7 | component: Image,
8 | title: 'components/Image',
9 | args: {
10 | src: 'https://placekitten.com/200/300',
11 | width: [200, 500],
12 | height: 500,
13 | } as ImageProps,
14 | };
15 |
16 | export const Basic = (args) => (
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
24 | const array = [...Object(Array(300)).keys()];
25 |
26 | export const LazyLoadStressTest = (args) => (
27 |
28 |
29 | The dimensions must be specified for this to work. So either height+width,
30 | flex-basis etc.. or a placeholder with the same dimensions
31 |
32 |
33 | {array.map((i) => (
34 |
41 | ))}
42 |
43 |
44 | );
45 |
--------------------------------------------------------------------------------
/src/styles/theme/colors.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | primary50: '#F0F9FF',
3 | primary100: '#E0F2FE',
4 | primary200: '#BAE6FD',
5 | primary300: '#7DD3FC',
6 | primary400: '#38BDF8',
7 | primary500: '#0EA5E9',
8 | primary600: '#0284C7',
9 | primary700: '#0369A1',
10 | primary800: '#075985',
11 | primary900: '#083853',
12 | secondary50: '#F5F3FF',
13 | secondary100: '#EDE9FE',
14 | secondary200: '#DDD6FE',
15 | secondary300: '#C4B5FD',
16 | secondary400: '#A78BFA',
17 | secondary500: '#8B5CF6',
18 | secondary600: '#7C3AED',
19 | secondary700: '#6D28D9',
20 | secondary800: '#541EAA',
21 | secondary900: '#3C137B',
22 | white: '#ffffff',
23 | grey50: '#FAFAFA',
24 | grey100: '#F4F4F5',
25 | grey200: '#E4E4E7',
26 | grey300: '#D4D4D8',
27 | grey400: '#A1A1AA',
28 | grey500: '#71717A',
29 | grey600: '#52525B',
30 | grey700: '#3F3F46',
31 | grey800: '#27272A',
32 | grey900: '#18181B',
33 | warning50: '#FEFCE8',
34 | warning100: '#FEF08A',
35 | warning300: '#FACC15',
36 | warning600: '#CA8A04',
37 | warning800: '#713F12',
38 | success50: '#F0FDF4',
39 | success100: '#BBF7D0',
40 | success300: '#4ADE80',
41 | success600: '#16A34A',
42 | success800: '#14532D',
43 | error50: '#FEF2F2',
44 | error100: '#FEE2E2',
45 | error300: '#F87171',
46 | error600: '#DC2626',
47 | error800: '#7F1D1D',
48 | transparent: 'rgba(255, 255, 255, 0);',
49 | } as const;
50 |
--------------------------------------------------------------------------------
/src/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Box, Flex, Heading, Text } from '@storyofams/react-ui';
2 | import { NextSeo } from 'next-seo';
3 |
4 | import { Background } from '~components/home/background';
5 |
6 | const getError = ({ res, err }) => {
7 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
8 | return { statusCode };
9 | };
10 |
11 | const getContent = ({ statusCode }) => {
12 | switch (statusCode) {
13 | case 401:
14 | return "It looks like you're not supposed to be here 👀";
15 | case 404:
16 | return 'We could not find the page you were looking for 🛰';
17 | case 500:
18 | return 'Our server had some trouble processing that request 🔥';
19 | default:
20 | return "Even we don't know what happened 🤯";
21 | }
22 | };
23 |
24 | const Error = ({ statusCode }) => {
25 | const content = getContent({ statusCode });
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 |
37 |
38 | {statusCode}
39 |
40 | {content}
41 |
42 | Take me home
43 |
44 |
45 |
46 | >
47 | );
48 | };
49 |
50 | Error.getInitialProps = ({ res, err }) => getError({ res, err });
51 |
52 | export default Error;
53 |
--------------------------------------------------------------------------------
/src/__tests__/pages/api/sitemap.test.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 | import { createMocks } from 'node-mocks-http';
3 | import sitemapHandler, { Sitemap } from '~pages/api/sitemap';
4 |
5 | describe('[api] sitemap', () => {
6 | it('should return a sitemap', (done) => {
7 | const { req, res } = createMocks(
8 | { url: '/api/sitemap' },
9 | { eventEmitter: EventEmitter },
10 | );
11 | const lastmod = new Date().toISOString();
12 |
13 | jest
14 | .spyOn(Sitemap.prototype, 'fetchMySitemapData')
15 | .mockImplementation(() => [{ url: 'http://test.com', lastmod }]);
16 |
17 | (sitemapHandler(req as any, res as any) as Promise)
18 | .then(() => {
19 | res.on('end', () => {
20 | expect(res._getStatusCode()).toBe(200);
21 | expect(res._getHeaders()).toHaveProperty(
22 | 'cache-control',
23 | 's-maxage=3600, stale-while-revalidate',
24 | );
25 | expect(res._getHeaders()).toHaveProperty('content-type', 'text/xml');
26 | expect(res._getData()).toContain('http://test.com/ ');
27 | expect(res._getData()).toContain(`${lastmod} `);
28 | done();
29 | });
30 | })
31 | .finally(() => jest.restoreAllMocks());
32 | });
33 |
34 | it('should fail gracefully', async () => {
35 | const { req, res } = createMocks({ url: '/api/sitemap' });
36 |
37 | jest
38 | .spyOn(Sitemap.prototype, 'fetchMySitemapData')
39 | .mockImplementation(() => {
40 | throw new Error();
41 | });
42 | await sitemapHandler(req as any, res as any);
43 |
44 | expect(res._getStatusCode()).toBe(500);
45 |
46 | jest.restoreAllMocks();
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/public/static/fonts/stylesheet.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Domaine Disp';
3 | src: url('DomaineDisp-Bold.woff2') format('woff2'),
4 | url('DomaineDisp-Bold.woff') format('woff');
5 | font-weight: bold;
6 | font-style: normal;
7 | font-display: swap;
8 | }
9 |
10 | @font-face {
11 | font-family: 'Inter';
12 | src: url('Inter-Bold.woff2') format('woff2'),
13 | url('Inter-Bold.woff') format('woff');
14 | font-weight: bold;
15 | font-style: normal;
16 | font-display: swap;
17 | }
18 |
19 | @font-face {
20 | font-family: 'Inter';
21 | src: url('Inter-BoldItalic.woff2') format('woff2'),
22 | url('Inter-BoldItalic.woff') format('woff');
23 | font-weight: bold;
24 | font-style: italic;
25 | font-display: swap;
26 | }
27 |
28 | @font-face {
29 | font-family: 'Inter';
30 | src: url('Inter-Regular.woff2') format('woff2'),
31 | url('Inter-Regular.woff') format('woff');
32 | font-weight: normal;
33 | font-style: normal;
34 | font-display: swap;
35 | }
36 |
37 | @font-face {
38 | font-family: 'Inter';
39 | src: url('Inter-Italic.woff2') format('woff2'),
40 | url('Inter-Italic.woff') format('woff');
41 | font-weight: normal;
42 | font-style: italic;
43 | font-display: swap;
44 | }
45 |
46 | @font-face {
47 | font-family: 'Inter';
48 | src: url('Inter-Medium.woff2') format('woff2'),
49 | url('Inter-Medium.woff') format('woff');
50 | font-weight: 500;
51 | font-style: normal;
52 | font-display: swap;
53 | }
54 |
55 | @font-face {
56 | font-family: 'Inter';
57 | src: url('Inter-MediumItalic.woff2') format('woff2'),
58 | url('Inter-MediumItalic.woff') format('woff');
59 | font-weight: 500;
60 | font-style: italic;
61 | font-display: swap;
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from 'next/document';
2 | import { resetId } from 'react-id-generator';
3 | import { ServerStyleSheet } from 'styled-components';
4 |
5 | export default class MyDocument extends Document {
6 | static async getInitialProps(ctx) {
7 | const sheet = new ServerStyleSheet();
8 | const originalRenderPage = ctx.renderPage;
9 | resetId();
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 | return {
20 | ...initialProps,
21 | styles: (
22 | <>
23 | {initialProps.styles}
24 | {sheet.getStyleElement()}
25 | >
26 | ),
27 | };
28 | } finally {
29 | sheet.seal();
30 | }
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
49 |
53 |
54 |
55 |
56 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Contributing
6 |
7 |
8 | ## Commit messages
9 |
10 | Uses [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) and husky to enforce them.
11 |
12 | In short commit messages should look like:
13 |
14 | ```bash
15 | [optional scope]:
16 | ```
17 |
18 | Examples:
19 |
20 | ```bash
21 | feat(box): added border prop for styled-system
22 | fix: fixed crashing issue on IE
23 | refactor(landing page): moved data fetching to swr
24 | ```
25 |
26 | Possible types: `fix`,`feat`,`refactor`,`docs`,`ci`,`test`,`chore`
27 |
28 | ### CLI
29 |
30 | The project is setup to enforce conventional commits, this can be daunting if this is the first time hearing about them. Luckily all of our projects come with a customized CLI to help you draft up conventional commits (emoji's included) 🤩
31 |
32 | So if you want to stay true to the conventional commit guidelines, please try out `yarn commit`. A prompt will lead you through setting up the commit (don't forget `git add` before running the command 😉).
33 |
34 | ## Testing
35 |
36 | Always make sure to add or update test(s) when adding or altering functionality. Doing this will ensure that the code keeps working the way it is intended to work.
37 |
38 | ### Why tests?
39 |
40 | - Less bugs
41 | - Higher quality software in less time
42 | - Automatic documentation
43 | - Tdd leads to better workflow
44 | - Writing tests takes very little time
45 |
46 | #### What to test
47 |
48 | [start here if you don't know what to test](https://kentcdodds.com/blog/write-tests)
49 |
50 | > Write tests. Not too many. Mostly integration.
51 |
52 | Don't test implementation details! Just test your component like you would use it in real life.
53 |
54 | The closer your tests resemble the way they are being used the more confidence they can give you.
55 |
56 | Testing examples for pretty much every use case are [here](https://github.com/kentcdodds/react-testing-library-course)
57 |
--------------------------------------------------------------------------------
/src/styles/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { Breakpoints } from '../styled';
2 | import colors from './colors';
3 | import space from './space';
4 |
5 | const theme = {
6 | colors,
7 | fontWeights: {
8 | regular: 400,
9 | medium: 500,
10 | semiBold: 600,
11 | bold: 700,
12 | },
13 | fonts: {
14 | heading: `Domaine Disp`,
15 | body: `Inter`,
16 | mono: `SFMono-Regular, Menlo, Monaco,C onsolas, "Liberation Mono", "Courier New", monospace`,
17 | },
18 | fontSizes: {
19 | 1.25: space['1.25'],
20 | 1.5: space['1.5'],
21 | 1.75: space['1.75'],
22 | 2: space['2'],
23 | 2.25: space['2.25'],
24 | 2.5: space['2.5'],
25 | 3: space['3'],
26 | 4: space['4'],
27 | 5: space['5'],
28 | 6: space['6'],
29 | 7: space['7'],
30 | 8: space['8'],
31 | 10: space['9'],
32 | 12: space['12'],
33 | 18: space['18'],
34 | 20: space['20'],
35 | root: space['1.75'],
36 | heading: space['4'],
37 | },
38 | lineHeights: {
39 | normal: 1,
40 | heading: 1.1,
41 | medium: 1.25,
42 | high: 1.6,
43 | },
44 | space: {
45 | ...space,
46 | mobileGutter: space['2'],
47 | },
48 | sizes: {
49 | maxWidth: 1140,
50 | },
51 | breakpoints: ['768px', '1024px', '1280px', '1440px'] as Breakpoints,
52 | zIndices: {
53 | hide: -1,
54 | base: 0,
55 | docked: 10,
56 | dropdown: 1000,
57 | sticky: 1100,
58 | banner: 1200,
59 | overlay: 1300,
60 | modal: 1400,
61 | popover: 1500,
62 | skipLink: 1600,
63 | toast: 1700,
64 | tooltip: 1800,
65 | },
66 | radii: {
67 | none: '0',
68 | xs: '4px',
69 | sm: '6px',
70 | md: '8px',
71 | lg: '16px',
72 | full: '9999px',
73 | },
74 | borders: {
75 | none: 0,
76 | '1px': '1px solid',
77 | '2px': '2px solid',
78 | '4px': '4px solid',
79 | },
80 | shadows: {
81 | sm: '0px 2px 0px rgba(0, 0, 0, 0.1), 0px 5px 10px rgba(0, 0, 0, 0.05)',
82 | md: '0px 2px 0px rgba(0, 0, 0, 0.1), 0px 5px 10px rgba(0, 0, 0, 0.05)',
83 | lg: '0px 2px 4px rgba(0, 0, 0, 0.1), 0px 10px 20px rgba(0, 0, 0, 0.1)',
84 | none: 'none',
85 | },
86 | };
87 |
88 | theme.breakpoints.sm = theme.breakpoints[0];
89 | theme.breakpoints.md = theme.breakpoints[1];
90 | theme.breakpoints.lg = theme.breakpoints[2];
91 | theme.breakpoints.xl = theme.breakpoints[3];
92 |
93 | export default theme;
94 |
--------------------------------------------------------------------------------
/src/components/common/Image/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, SystemProps } from '@storyofams/react-ui';
2 | import { pick, omit } from '@styled-system/props';
3 | import NextImage, { ImageProps as NextImageProps } from 'next/image';
4 | import { HeightProps, WidthProps } from 'styled-system';
5 |
6 | const getHighestValue = (value: any): number | string => {
7 | switch (typeof value) {
8 | case 'number':
9 | return value;
10 | case 'object':
11 | if (Array.isArray(value)) {
12 | return value
13 | .map(getHighestValue)
14 | .sort((a, b) => Number(b) - Number(a))[0];
15 | }
16 | return Object.values(value)
17 | .map(getHighestValue)
18 | .sort((a, b) => Number(b) - Number(a))[0];
19 | case 'string':
20 | if (value.includes('%')) {
21 | return value;
22 | }
23 | return parseInt(value);
24 | }
25 | };
26 |
27 | export type ImageProps = Omit &
28 | Omit &
29 | (
30 | | {
31 | layout: 'fill';
32 | width?: WidthProps['width'];
33 | height?: HeightProps['height'];
34 | }
35 | | {
36 | width: WidthProps['width'];
37 | height: HeightProps['height'];
38 | layout?: 'fixed' | 'intrinsic' | 'responsive';
39 | }
40 | );
41 |
42 | /**
43 | * @description Image component which uses Rebass as Next's Image component. When you use this component and you're not certain of the source
44 | * domain of the image (i.e. user input) use, make sure to use the `unoptimized` prop. Otherwise declare the domain of the image in the `next.config.js`
45 | */
46 |
47 | export const Image = (props: ImageProps) => {
48 | const nextImageProps = omit(props) as Omit<
49 | NextImageProps,
50 | 'width' | 'height'
51 | >;
52 | const imageProps = pick(props);
53 |
54 | if (props.layout === 'fill') {
55 | return (
56 |
57 | {/** @ts-ignore **/}
58 |
59 |
60 | );
61 | }
62 |
63 | return (
64 |
65 | {/** @ts-ignore **/}
66 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | import { withPasswordProtect } from '@storyofams/next-password-protect';
4 | import { DefaultSeo } from 'next-seo';
5 | import { AppProps } from 'next/app';
6 | import { useRouter } from 'next/router';
7 | import NProgress from 'nprogress';
8 | import { ReactQueryDevtools } from 'react-query/devtools';
9 |
10 | import { Providers, UserAgent } from '~components';
11 | import { seo } from '~config';
12 | import { initSentry } from '~lib';
13 | import useDetectKeyboard from '~hooks/useDetectKeyboard';
14 | import { pageview } from '~lib/gtag';
15 | import CSSreset from '~styles/CSSreset';
16 |
17 | import 'nprogress/nprogress.css';
18 |
19 | import '../../public/static/fonts/stylesheet.css';
20 |
21 | if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
22 | initSentry();
23 | }
24 |
25 | const MyApp = ({ pageProps, Component }: AppProps) => {
26 | const router = useRouter();
27 | const progressTimer = useRef(null);
28 |
29 | useDetectKeyboard();
30 |
31 | useEffect(() => {
32 | const startProgress = () => {
33 | if (!progressTimer.current) {
34 | progressTimer.current = setTimeout(NProgress.start, 120);
35 | }
36 | };
37 |
38 | const onComplete = (url) => {
39 | pageview(url);
40 | endProgress(url);
41 | };
42 |
43 | const endProgress = (err) => {
44 | if (progressTimer.current) {
45 | clearTimeout(progressTimer.current);
46 | progressTimer.current = null;
47 |
48 | if (err?.cancelled) {
49 | NProgress.set(0.0);
50 | NProgress.remove();
51 | } else {
52 | NProgress.done();
53 | }
54 | }
55 | };
56 |
57 | router.events.on('routeChangeStart', startProgress);
58 | router.events.on('routeChangeComplete', onComplete);
59 | router.events.on('routeChangeError', endProgress);
60 |
61 | return () => {
62 | router.events.off('routeChangeStart', startProgress);
63 | router.events.off('routeChangeComplete', onComplete);
64 | router.events.off('routeChangeError', endProgress);
65 | };
66 | });
67 |
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default process.env.PASSWORD_PROTECT
80 | ? withPasswordProtect(MyApp)
81 | : MyApp;
82 |
--------------------------------------------------------------------------------
/.cz-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // add additional standard scopes here
3 | scopes: [{ name: "accounts" }, { name: "admin" }],
4 | // use this to permanently skip any questions by listing the message key as a string
5 | skipQuestions: [],
6 |
7 | /* DEFAULT CONFIG */
8 | messages: {
9 | type: "What type of changes are you committing:",
10 | scope: "\nEnlighten us with the scope (optional):",
11 | customScope: "Add the scope of your liking:",
12 | subject: "Write a short and simple description of the change:\n",
13 | body:
14 | 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
15 | breaking: "List any BREAKING CHANGES (optional):\n",
16 | footer:
17 | "List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n",
18 | confirmCommit: "Are you sure you the above looks right?",
19 | },
20 | types: [
21 | {
22 | value: "fix",
23 | name: "🐛 fix: Changes that fix a bug",
24 | emoji: "🐛",
25 | },
26 | {
27 | value: "feat",
28 | name: " 🚀 feat: Changes that introduce a new feature",
29 | emoji: "🚀",
30 | },
31 | {
32 | value: "refactor",
33 | name:
34 | "🔍 refactor: Changes that neither fixes a bug nor adds a feature",
35 | emoji: "🔍",
36 | },
37 | {
38 | value: "test",
39 | name: "💡 test: Adding missing tests",
40 | emoji: "💡",
41 | },
42 | {
43 | value: "style",
44 | name:
45 | "💅 style: Changes that do not impact the code base \n (white-space, formatting, missing semi-colons, etc)",
46 | emoji: "💅",
47 | },
48 | {
49 | value: "docs",
50 | name: "📝 docs: Changes to the docs",
51 | emoji: "📝",
52 | },
53 | {
54 | value: "chore",
55 | name:
56 | "🤖 chore: Changes to the build process or auxiliary tools\n and or libraries such as auto doc generation",
57 | emoji: "🤖",
58 | },
59 | {
60 | value: "ci",
61 | name:
62 | "👾 ci: Changes related to setup and usage of CI",
63 | emoji: "👾",
64 | },
65 | ],
66 | allowTicketNumber: false,
67 | isTicketNumberRequired: false,
68 | ticketNumberPrefix: "#",
69 | ticketNumberRegExp: "\\d{1,5}",
70 | allowCustomScopes: true,
71 | allowBreakingChanges: ["feat", "fix", "chore"],
72 | breakingPrefix: "🚧 BREAKING CHANGES 🚧",
73 | footerPrefix: "CLOSES",
74 | subjectLimit: 100,
75 | };
76 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | run-tests:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Checkout repo
11 | uses: actions/checkout@v2
12 |
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v2
15 | with:
16 | node-version: 12
17 |
18 | - name: Get yarn cache directory path
19 | id: yarn-cache-dir-path
20 | run: echo "::set-output name=dir::$(yarn cache dir)"
21 |
22 | - name: Cache yarn dependencies
23 | uses: actions/cache@v2
24 | id: yarn-cache
25 | with:
26 | path: |
27 | ${{ steps.yarn-cache-dir-path.outputs.dir }}
28 | node_modules
29 | */*/node_modules
30 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
31 | restore-keys: |
32 | ${{ runner.os }}-yarn-
33 |
34 | - name: Install modules
35 | run: yarn
36 |
37 | - name: Run tests
38 | run: yarn test
39 |
40 | run-cypress:
41 | runs-on: ubuntu-latest
42 |
43 | steps:
44 | - uses: actions/checkout@v2
45 |
46 | - name: Get deployment URL
47 | uses: patrickedqvist/wait-for-vercel-preview@master
48 | id: waitFor200
49 | with:
50 | token: ${{ secrets.GITHUB_TOKEN }}
51 | max_timeout: 300
52 |
53 | - name: Cypress run
54 | uses: cypress-io/github-action@v2
55 | with:
56 | config: 'baseUrl=${{ steps.waitFor200.outputs.url }}'
57 |
58 | # screenshots are only created on failure, thus we use the "failure()" condition
59 | - uses: actions/upload-artifact@v1
60 | if: failure()
61 | with:
62 | name: cypress-screenshots
63 | path: cypress/screenshots
64 |
65 | # videos are always created, thus we use the "always()" condition
66 | - uses: actions/upload-artifact@v1
67 | if: always()
68 | with:
69 | name: cypress-videos
70 | path: cypress/videos
71 |
72 | run-lighthouse:
73 | runs-on: ubuntu-latest
74 |
75 | steps:
76 | - uses: actions/checkout@v2
77 | - run: mkdir /tmp/artifacts
78 |
79 | - name: Get deployment URL
80 | uses: patrickedqvist/wait-for-vercel-preview@master
81 | id: waitFor200
82 | with:
83 | token: ${{ secrets.GITHUB_TOKEN }}
84 | max_timeout: 300
85 |
86 | - name: Run Lighthouse
87 | uses: foo-software/lighthouse-check-action@master
88 | with:
89 | outputDirectory: /tmp/artifacts
90 | urls: '${{ steps.waitFor200.outputs.url }}'
91 |
92 | - name: Upload artifacts
93 | uses: actions/upload-artifact@master
94 | with:
95 | name: Lighthouse reports
96 | path: /tmp/artifacts
97 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({
2 | enabled: process.env.ANALYZE === 'true',
3 | });
4 |
5 | const SentryWebpackPlugin = require('@sentry/webpack-plugin');
6 | // const withOffline = require('next-offline')
7 | // Use the hidden-source-map option when you don't want the source maps to be
8 | // publicly available on the servers, only to the error reporting
9 | const withSourceMaps = require('@zeit/next-source-maps');
10 | const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
11 |
12 | // Use the SentryWebpack plugin to upload the source maps during build step
13 | const {
14 | NEXT_PUBLIC_SENTRY_DSN: SENTRY_DSN,
15 | SENTRY_ORG,
16 | SENTRY_PROJECT,
17 | SENTRY_AUTH_TOKEN,
18 | NODE_ENV,
19 | CI_COMMIT_SHA,
20 | PRODUCTION,
21 | } = process.env;
22 |
23 | process.env.SENTRY_DSN = SENTRY_DSN;
24 | const basePath = '';
25 |
26 | module.exports = withBundleAnalyzer(
27 | withSourceMaps({
28 | env: {
29 | // Make the COMMIT_SHA available to the client so that Sentry events can be
30 | // marked for the release they belong to. It may be undefined if running
31 | // outside of Vercel
32 | NEXT_PUBLIC_COMMIT_SHA: CI_COMMIT_SHA,
33 | // Protect staging with a password
34 | PASSWORD_PROTECT: process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging',
35 | },
36 | webpack(config, options) {
37 | config.resolve.plugins = [
38 | new TsconfigPathsPlugin({ extensions: config.resolve.extensions }),
39 | ];
40 |
41 | config.module.rules.push({
42 | test: /\.svg$/,
43 | use: [
44 | { loader: '@svgr/webpack', options: { icon: true, svgo: false } },
45 | ],
46 | });
47 |
48 | config.module.rules.push({
49 | test: /\.svg$/,
50 | use: [
51 | { loader: '@svgr/webpack', options: { icon: true, svgo: false } },
52 | ],
53 | });
54 |
55 | // In `pages/_app.js`, Sentry is imported from @sentry/browser. While
56 | // @sentry/node will run in a Node.js environment. @sentry/node will use
57 | // Node.js-only APIs to catch even more unhandled exceptions.
58 | //
59 | // This works well when Next.js is SSRing your page on a server with
60 | // Node.js, but it is not what we want when your client-side bundle is being
61 | // executed by a browser.
62 | //
63 | // Luckily, Next.js will call this webpack function twice, once for the
64 | // server and once for the client. Read more:
65 | // https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config
66 | //
67 | // So ask Webpack to replace @sentry/node imports with @sentry/browser when
68 | // building the browser's bundle
69 | if (!options.isServer) {
70 | config.resolve.alias['@sentry/node'] = '@sentry/browser';
71 | }
72 |
73 | // Define an environment variable so source code can check whether or not
74 | // it's running on the server so we can correctly initialize Sentry
75 | config.plugins.push(
76 | new options.webpack.DefinePlugin({
77 | 'process.env.NEXT_IS_SERVER': JSON.stringify(
78 | options.isServer.toString(),
79 | ),
80 | }),
81 | );
82 |
83 | // When all the Sentry configuration env variables are available/configured
84 | // The Sentry webpack plugin gets pushed to the webpack plugins to build
85 | // and upload the source maps to sentry.
86 | // This is an alternative to manually uploading the source maps
87 | // Note: This is disabled in development mode.
88 | if (
89 | SENTRY_DSN &&
90 | SENTRY_ORG &&
91 | SENTRY_PROJECT &&
92 | SENTRY_AUTH_TOKEN &&
93 | CI_COMMIT_SHA &&
94 | NODE_ENV === 'production' &&
95 | PRODUCTION
96 | ) {
97 | config.plugins.push(
98 | new SentryWebpackPlugin({
99 | include: '.next',
100 | ignore: ['node_modules'],
101 | stripPrefix: ['webpack://_N_E/'],
102 | urlPrefix: `~${basePath}/_next`,
103 | release: CI_COMMIT_SHA,
104 | }),
105 | );
106 | }
107 |
108 | return config;
109 | },
110 | basePath,
111 | async redirects() {
112 | return [
113 | {
114 | source: '/sitemap.xml',
115 | destination: '/api/sitemap',
116 | permanent: true,
117 | },
118 | ];
119 | },
120 | }),
121 | );
122 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-boilerplate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "commit": "./node_modules/cz-customizable/standalone.js",
10 | "test": "jest --coverage",
11 | "test:watch": "jest --watch",
12 | "lint": "eslint \"**/*.+(js|jsx|ts|tsx|mdx)\"",
13 | "ts-coverage": "typescript-coverage-report",
14 | "storybook": "start-storybook -s ./public/static -p 6006",
15 | "build-storybook": "build-storybook",
16 | "semantic-release": "semantic-release",
17 | "analyze": "ANALYZE=true next build",
18 | "cy:install": "cypress install",
19 | "cy:run": "cypress run",
20 | "cy:open": "cypress open"
21 | },
22 | "dependencies": {
23 | "@next/bundle-analyzer": "11.0.1",
24 | "@reach/alert": "0.15.3",
25 | "@reach/checkbox": "0.15.3",
26 | "@reach/dialog": "0.15.3",
27 | "@sentry/browser": "6.9.0",
28 | "@sentry/integrations": "6.9.0",
29 | "@sentry/node": "6.9.0",
30 | "@storyofams/next-api-decorators": "1.5.5",
31 | "@storyofams/next-password-protect": "^1.5.8",
32 | "@storyofams/react-helpers": "0.3.8",
33 | "@storyofams/react-ui": "1.2.0",
34 | "@styled-system/css": "5.1.5",
35 | "@styled-system/props": "5.1.5",
36 | "@svgr/webpack": "5.5.0",
37 | "@zeit/next-source-maps": "0.0.3",
38 | "date-fns": "2.22.1",
39 | "fontfaceobserver": "2.1.0",
40 | "framer-motion": "4.1.17",
41 | "next": "11.1.1",
42 | "next-seo": "4.26.0",
43 | "nprogress": "^0.2.0",
44 | "react": "17.0.2",
45 | "react-dom": "17.0.2",
46 | "react-flatpickr": "3.10.7",
47 | "react-hook-form": "7.11.1",
48 | "react-id-generator": "3.0.1",
49 | "react-lazyload": "3.2.0",
50 | "react-query": "3.21.1",
51 | "react-select": "4.3.1",
52 | "sitemap": "7.0.0",
53 | "styled-components": "5.3.0",
54 | "styled-system": "5.1.5",
55 | "system-props": "0.20.0",
56 | "yup": "0.32.9"
57 | },
58 | "devDependencies": {
59 | "@babel/core": "7.14.6",
60 | "@babel/plugin-proposal-decorators": "7.14.5",
61 | "@babel/runtime-corejs2": "7.14.6",
62 | "@commitlint/cli": "12.1.4",
63 | "@commitlint/config-conventional": "12.1.4",
64 | "@jest/types": "27.0.6",
65 | "@semantic-release/changelog": "5.0.1",
66 | "@sentry/webpack-plugin": "1.16.0",
67 | "@storybook/addon-actions": "6.3.4",
68 | "@storybook/addon-controls": "6.3.4",
69 | "@storybook/addon-docs": "6.3.4",
70 | "@storybook/addon-links": "6.3.4",
71 | "@storybook/addon-viewport": "6.3.4",
72 | "@storybook/addons": "6.3.4",
73 | "@storybook/react": "6.3.4",
74 | "@storyofams/eslint-config-ams": "1.1.4",
75 | "@testing-library/cypress": "7.0.6",
76 | "@testing-library/jest-dom": "^5.14.1",
77 | "@testing-library/react": "12.0.0",
78 | "@types/jest": "26.0.24",
79 | "@types/node": "16.3.3",
80 | "@types/react": "17.0.14",
81 | "@types/react-dom": "17.0.9",
82 | "@types/react-select": "4.0.17",
83 | "@types/rebass": "4.0.9",
84 | "@types/styled-components": "5.1.11",
85 | "@types/styled-system": "5.1.12",
86 | "@types/yup": "0.29.13",
87 | "@typescript-eslint/eslint-plugin": "4.28.3",
88 | "@typescript-eslint/parser": "4.28.3",
89 | "awesome-typescript-loader": "5.2.1",
90 | "babel-eslint": "10.1.0",
91 | "babel-loader": "8.2.2",
92 | "babel-plugin-parameter-decorator": "1.0.16",
93 | "babel-plugin-styled-components": "1.13.2",
94 | "babel-plugin-transform-typescript-metadata": "0.3.2",
95 | "babel-preset-react-app": "10.0.0",
96 | "cypress": "7.7.0",
97 | "cypress-hmr-restarter": "2.0.2",
98 | "cz-customizable": "git+https://github.com/storyofams/cz-customizable.git#v6.3.2",
99 | "eslint": "7.31.0",
100 | "eslint-config-next": "^11.0.1",
101 | "eslint-config-prettier": "8.3.0",
102 | "eslint-import-resolver-alias": "1.1.2",
103 | "eslint-plugin-cypress": "2.11.3",
104 | "eslint-plugin-import": "2.23.4",
105 | "eslint-plugin-jsx-a11y": "6.4.1",
106 | "eslint-plugin-mdx": "1.14.1",
107 | "eslint-plugin-prettier": "3.4.0",
108 | "eslint-plugin-react": "7.24.0",
109 | "eslint-plugin-react-hooks": "4.2.0",
110 | "fork-ts-checker-webpack-plugin": "6.2.12",
111 | "husky": "7.0.1",
112 | "jest": "27.0.6",
113 | "jest-axe": "5.0.1",
114 | "lint-staged": "11.0.1",
115 | "node-mocks-http": "1.10.1",
116 | "prettier": "2.3.2",
117 | "react-docgen-typescript-loader": "3.7.2",
118 | "react-select-event": "5.3.0",
119 | "react-test-renderer": "17.0.2",
120 | "semantic-release": "17.4.4",
121 | "ts-jest": "27.0.3",
122 | "ts-loader": "9.2.3",
123 | "ts-node": "10.1.0",
124 | "tsconfig-paths-webpack-plugin": "3.5.1",
125 | "typescript": "4.3.5",
126 | "typescript-coverage-report": "0.6.0"
127 | },
128 | "typeCoverage": {
129 | "atLeast": 90
130 | },
131 | "eslintConfig": {
132 | "extends": [
133 | "@storyofams/eslint-config-ams/web"
134 | ]
135 | },
136 | "commitlint": {
137 | "extends": [
138 | "@commitlint/config-conventional"
139 | ]
140 | },
141 | "config": {
142 | "commitizen": {
143 | "path": "node_modules/cz-customizable"
144 | }
145 | },
146 | "lint-staged": {
147 | "**/*.+(js|jsx|ts|tsx|mdx)": [
148 | "eslint --fix"
149 | ]
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/docs/introduction.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/addon-docs/blocks';
2 |
3 |
4 |
5 | # Next Boilerplate
6 |
7 | Next boilerplate starter template
8 |
9 | ## 🏁 Introduction
10 |
11 | [storybook](https://storybook.js.org/) is an open-source platform that allows developers to keep a consistent overview of the pieces and pages that all make the platform.
12 | In their own words;
13 |
14 | > Storybook is an open source tool for developing UI components in isolation for React, Vue, and Angular. It makes building stunning UIs organized and efficient.
15 |
16 | There are various implemenations possible with storybook and the effectiveness of those implemenations differ per project.
17 | For our projects we use a simplistic, yet effective approach of displaying pure components and only combine those components into pages if it makes sense to do so.
18 |
19 | ---
20 |
21 | ### 🛠 How it works
22 |
23 | When initiating a project we start out by working out all the different components that exist within the desings.
24 | Those components are created and then replicated in something that storybook calls 'stories'. Stories are a simple representation of the component and the different varations it has within the platform.
25 |
26 | These stories are fun look at on it's own but the fun is just starting. [Addons](https://github.com/storybookjs/storybook/tree/master/addons) for storybook is where the party really starts.
27 |
28 | ### ⚡️ [Addons](https://storybook.js.org/addons/)
29 |
30 | There is a big landscape of storybook owned/maintained addons and some community build ones. It's easy to get overwhelmed when creating a the perfect setup but I think there is a minimal set of addons that can jumpstart any project.
31 | This is the reason why we are making use of a small selection addons of the various addon possibilities within storybook. The following addons are added to this project; [docs](https://github.com/storybookjs/storybook/tree/master/addons/docs), [controls](https://github.com/storybookjs/storybook/tree/master/addons/controls), [actions](https://github.com/storybookjs/storybook/tree/master/addons/actions), [viewport](https://github.com/storybookjs/storybook/tree/master/addons/viewport).
32 | These addons allow for the components we make to be viewed in more documented way, allows for manipulating of the components and enables to choose diferent screen-size to view how that impacts the components (and pages).
33 |
34 | #### 💅 [Theming](https://github.com/storybookjs/storybook/tree/master/addons/theming)
35 |
36 | We all love our colours, fonts and logo's. Especially our logo's!
37 | With this addon we override the default (and boring, but solid) theming storybook provides out of the box and inject it with our owns settings.
38 |
39 | If you are a developer and are looking on how to make sure your storybook project utilizes brand colours, logo's and fonts?
40 | [Go to this section]() in the developer section.
41 |
42 | #### 📚 [Docs](https://github.com/storybookjs/storybook/tree/master/addons/docs)
43 |
44 | This addon enables a different view from the canvas view. It enables to view all variations within one page and gives more of a documentation style of view.
45 | It has a collapsible box to show the actual code example that was used in the story and displays the different properties that can be passed to the component to influence it's appreance.
46 |
47 | This is the perfect place to show the product to someone at a glance but still get a detailed way of possibilities and possible implementations.
48 |
49 | #### ⚙️ [Controls](https://github.com/storybookjs/storybook/tree/master/addons/controls)
50 |
51 | This awesome new development within storybook alters the way developers write their stories.
52 | Where we used to manually write every property out by hand and define its possible values this is now autogenerated (especially nice for typescript) and manipulates the appearance of the component in real time.
53 |
54 | #### 📝 [actions](https://github.com/storybookjs/storybook/tree/master/addons/actions)
55 |
56 | One of the older addons from this list, but it has been steady on the list since it's inception. This allows to visualize, in the form of logging, certain actions that can be initiated through certain actions.
57 |
58 | The most simple example that demonstrates the purpose of this addon is a button. A button can be used for many different things (code wise) but is only used for one purpose for the customer, which is to click the button.
59 | Some click actions can be recreated within storybook but when writing stupidly simple stories (which is where storybooks thrives by) adding logic just to make the button work is too much overhead.
60 | In this case we could still pass a function to the button by replacing all that logic with a simple `action('clicked')`.
61 | Which when fired will log this in a way that is visible for you.
62 |
63 | This allows for rapid development and gives a clear indication of the 'action(s)' a user executes during storybook usage.
64 |
65 | #### 📱 [viewport](https://github.com/storybookjs/storybook/tree/master/addons/viewport)
66 |
67 | Viewport is an enhancer that powers up storybook with the absolute powers of responsiveness. It allows to choose a viewport in which the component will be displayed. Very usefull if you want to check how the components looks on different sized screens.
68 |
69 | By default we extend the breakpoints to be part of the viewports because those are the screen sizes on which the design differs. Besides the breakpoints we also make use of the default extensive list of devices provided by addon;
70 |
71 | | OS | Devices |
72 | | ------- | -------------------------------------------------------------------------------------------------------------------------- |
73 | | iOS | iPhone 5, iPhone 6, iPhone 6 plus, iPhone 8 plus, iPhone X, iPhone XR, iPhone XS Max, iPad, iPad Pro 10.5', iPad Pro 12.9' |
74 | | Android | Galaxy S5, Galaxy S9, Nexus 5X, Nexus 6P, Pixel, Pixel XL |
75 |
76 | If you feel like you're missing a viewport or device feel free to submit a request to add a specific device.
77 |
78 | ---
79 |
80 | _❗️This document is subject to change over time and we reserve the right to do so without any obligations or restriction❗️_
81 |
--------------------------------------------------------------------------------
/src/styles/CSSreset.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle, css } from 'styled-components';
2 |
3 | const themeStyles = ({ theme }) => css`
4 | html {
5 | font-size: ${theme.fontSizes.root}px;
6 | font-weight: ${theme.fontWeights.regular};
7 |
8 | /* outline only when the user is using tab */
9 | &:not(.user-is-tabbing) {
10 | a[href],
11 | area[href],
12 | input:not([disabled]),
13 | select:not([disabled]),
14 | textarea:not([disabled]),
15 | button:not([disabled]),
16 | iframe,
17 | [tabindex],
18 | [contentEditable='true'] {
19 | outline: none;
20 | }
21 | }
22 | }
23 |
24 | .hidden {
25 | border: 0;
26 | clip: rect(0 0 0 0);
27 | height: 1px;
28 | width: 1px;
29 | margin: -1px;
30 | padding: 0;
31 | overflow: hidden;
32 | position: absolute;
33 | }
34 | `;
35 |
36 | const CSSreset = createGlobalStyle(
37 | ({ theme }) => css`
38 | html {
39 | line-height: 1.15;
40 | -webkit-text-size-adjust: 100%;
41 | }
42 | body {
43 | margin: 0;
44 | }
45 | main {
46 | display: block;
47 | }
48 | hr {
49 | box-sizing: content-box;
50 | height: 0;
51 | overflow: visible;
52 | }
53 | pre {
54 | font-family: monospace, monospace;
55 | font-size: 1em;
56 | }
57 | a {
58 | background-color: transparent;
59 | }
60 | abbr[title] {
61 | border-bottom: none;
62 | text-decoration: underline;
63 | -webkit-text-decoration: underline dotted;
64 | text-decoration: underline dotted;
65 | }
66 | b,
67 | strong {
68 | font-weight: bolder;
69 | }
70 | code,
71 | kbd,
72 | samp {
73 | font-family: monospace, monospace;
74 | font-size: 1em;
75 | }
76 | small {
77 | font-size: 80%;
78 | }
79 | sub,
80 | sup {
81 | font-size: 75%;
82 | line-height: 0;
83 | position: relative;
84 | vertical-align: baseline;
85 | }
86 | sub {
87 | bottom: -0.25em;
88 | }
89 | sup {
90 | top: -0.5em;
91 | }
92 | img {
93 | border-style: none;
94 | }
95 | button,
96 | input,
97 | optgroup,
98 | select,
99 | textarea {
100 | font-family: inherit;
101 | font-size: 100%;
102 | line-height: 1.15;
103 | margin: 0;
104 | }
105 | button {
106 | border: none;
107 | margin: 0;
108 | padding: 0;
109 | width: auto;
110 | overflow: visible;
111 | text-align: inherit;
112 | background: transparent;
113 |
114 | color: inherit;
115 | font: inherit;
116 |
117 | line-height: normal;
118 |
119 | -webkit-font-smoothing: inherit;
120 | -moz-osx-font-smoothing: inherit;
121 |
122 | -webkit-appearance: none;
123 | }
124 | button,
125 | input {
126 | overflow: visible;
127 | }
128 | button,
129 | select {
130 | text-transform: none;
131 | }
132 | button::-moz-focus-inner,
133 | [type='button']::-moz-focus-inner,
134 | [type='reset']::-moz-focus-inner,
135 | [type='submit']::-moz-focus-inner {
136 | border-style: none;
137 | padding: 0;
138 | }
139 | fieldset {
140 | padding: 0.35em 0.75em 0.625em;
141 | }
142 | legend {
143 | box-sizing: border-box;
144 | color: inherit;
145 | display: table;
146 | max-width: 100%;
147 | padding: 0;
148 | white-space: normal;
149 | }
150 | progress {
151 | vertical-align: baseline;
152 | }
153 | textarea {
154 | overflow: auto;
155 | }
156 | [type='checkbox'],
157 | [type='radio'] {
158 | box-sizing: border-box;
159 | padding: 0;
160 | }
161 | [type='number']::-webkit-inner-spin-button,
162 | [type='number']::-webkit-outer-spin-button {
163 | -webkit-appearance: none !important;
164 | }
165 | input[type='number'] {
166 | -moz-appearance: textfield;
167 | }
168 | [type='search'] {
169 | -webkit-appearance: textfield;
170 | outline-offset: -2px;
171 | }
172 | [type='search']::-webkit-search-decoration {
173 | -webkit-appearance: none !important;
174 | }
175 | ::-webkit-file-upload-button {
176 | -webkit-appearance: button;
177 | font: inherit;
178 | }
179 | details {
180 | display: block;
181 | }
182 | summary {
183 | display: list-item;
184 | }
185 | template {
186 | display: none;
187 | }
188 | [hidden] {
189 | display: none !important;
190 | }
191 | html {
192 | box-sizing: border-box;
193 | font-family: sans-serif;
194 | }
195 | *,
196 | *::before,
197 | *::after {
198 | box-sizing: border-box;
199 | }
200 | blockquote,
201 | dl,
202 | dd,
203 | h1,
204 | h2,
205 | h3,
206 | h4,
207 | h5,
208 | h6,
209 | hr,
210 | figure,
211 | p,
212 | pre {
213 | margin: 0;
214 | }
215 | button {
216 | background: transparent;
217 | padding: 0;
218 | }
219 | fieldset {
220 | margin: 0;
221 | padding: 0;
222 | }
223 | ol,
224 | ul {
225 | margin: 0;
226 | padding: 0;
227 | }
228 | html {
229 | font-family: ${theme.fonts.body};
230 | line-height: 1.5;
231 | -webkit-font-smoothing: antialiased;
232 | -webkit-text-size-adjust: 100%;
233 | text-rendering: optimizelegibility;
234 | }
235 | hr {
236 | border-top-width: 1px;
237 | }
238 | textarea {
239 | resize: vertical;
240 | }
241 | button,
242 | [role='button'] {
243 | cursor: pointer;
244 | }
245 | button::-moz-focus-inner {
246 | border: 0 !important;
247 | }
248 | table {
249 | border-collapse: collapse;
250 | }
251 | h1,
252 | h2,
253 | h3,
254 | h4,
255 | h5,
256 | h6 {
257 | font-family: ${theme.fonts.heading};
258 | }
259 | a {
260 | color: inherit;
261 | text-decoration: inherit;
262 | }
263 | button,
264 | input,
265 | optgroup,
266 | select,
267 | textarea {
268 | padding: 0;
269 | line-height: inherit;
270 | color: inherit;
271 | }
272 | pre,
273 | code,
274 | kbd,
275 | samp {
276 | font-family: ${theme.fonts.mono};
277 | }
278 | img,
279 | svg,
280 | video,
281 | canvas,
282 | audio,
283 | iframe,
284 | embed,
285 | object {
286 | display: block;
287 | vertical-align: middle;
288 | }
289 | img,
290 | video {
291 | max-width: 100%;
292 | height: auto;
293 | }
294 |
295 | #nprogress {
296 | z-index: 9999999;
297 |
298 | .bar {
299 | z-index: 9999999;
300 | background: ${theme.colors.primary500};
301 | }
302 |
303 | .peg {
304 | box-shadow: 0 0 10px ${theme.colors.primary500},
305 | 0 0 5px ${theme.colors.primary500};
306 | }
307 |
308 | .spinner {
309 | display: none;
310 | }
311 | }
312 |
313 | ${themeStyles}
314 | `,
315 | );
316 |
317 | export default CSSreset;
318 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
20 |
21 | ---
22 |
23 |
24 | Boilerplate to enable teams to build production grade, highly scalable and flexible react applications with Next.js
25 |
26 |
27 | ---
28 |
29 | # What's included? ([preview deployment](https://next-boilerplate-storyofams.vercel.app/))
30 |
31 | | Name | description |
32 | | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33 | | [@storyofams/next-api-decorators](https://github.com/storyofams/next-api-decorators) | Collection of decorators to create typed Next.js API routes, with easy request validation and transformation. [View docs](https://next-api-decorators.vercel.app/) |
34 | | [@storyofams/react-ui](https://github.com/storyofams/react-ui) | Collection of UI components build to create a production grade front-end experience. [View components](https://react-ui-storyofams.vercel.app/) |
35 | | [@storyofams/next-password-protect](https://github.com/storyofams/next-password-protect) | Password protect your Next.js deployments. [View demo (Password is secret)](https://next-boilerplate-ten.vercel.app/) |
36 | | [cypress](https://www.cypress.io/) | [cypress-testing-library](https://testing-library.com/docs/cypress-testing-library/intro/) implemented throughout. Be sure to have a look at [their docs](https://testing-library.com/docs/cypress-testing-library/intro/) |
37 | | [date-fns](https://date-fns.org/) | Enabling data manipulation in a comprehensive, yet humanly understandable fashion |
38 | | [eslint](https://github.com/eslint/eslint) | Ensures best practices are top of mind. Uses [@storyofams/eslint-config-ams](https://github.com/storyofams/eslint-config-ams) to get up and running quickly. |
39 | | [husky](https://github.com/typicode/husky) | Used to trigger specific actions on commit (linting files and commit messages) |
40 | | [jest](https://jestjs.io/) | [react-testing-library](https://testing-library.com/docs/react-testing-library/intro/) implemented throughout. Be sure to have a look at [their docs](https://testing-library.com/docs/react-testing-library/example-intro/) |
41 | | [sentry](https://sentry.io/welcome/) | Integration to toggle the sentry implementation on or off without the hassle |
42 | | [sitemap-handler](https://github.com/storyofams/next-boilerplate/blob/master/src/pages/api/sitemap.ts) | Simple implementation for xml-sitemap creation. |
43 | | [storybook](https://storybook.js.org/) | Storybook is an open source tool for building UI components and pages in isolation. It streamlines UI development, testing, and documentation. |
44 | | [SWR](https://swr.vercel.app/) | For all your data-handling needs (might get replaced by react-query in the near future) |
45 | | [typescript](https://www.typescriptlang.org/docs/) | Because building a javascript in this day and age git just doesn't feel right without it |
46 |
47 | ## Getting started
48 | *While we are working on a fancy generator* to get the generics bits inserted for you, the easiest thing to do is to **clone the repo**.
49 |
50 | Replace the theme file with your own and go through the files and folders to replace "Story of AMS" references with your own.
51 |
52 | ### Theme
53 |
54 | Basic theming is located in `src/styles/theme` folder.
55 |
56 | For more information on theming and styling see [@storyofams/react-ui](https://github.com/storyofams/react-ui).
57 |
58 | ## File structure
59 |
60 | ```md
61 | - src
62 | - components
63 | - home // components specific to the home page
64 | - common // for all shared components
65 | - List
66 | - list.test.tsx
67 | - list.stories.tsx
68 | - index.tsx // actual component lives here
69 | - config // constants & env variable export
70 | - hooks // custom hooks
71 | - lib // utils, helpers, etc
72 | - pages
73 | - styles // theme folder location (& css-reset + typesript)
74 | ```
75 |
76 | ## SEO
77 |
78 | Uses [next-seo](https://github.com/garmeeh/next-seo).
79 |
80 | Utilizes a default config to get up and running quickly (see `src/config`).
81 | Always include a ` ` tag with minimal information included on page level.
82 |
83 | ## Sentry
84 |
85 | This project comes out of the box with a sample implementation of [Sentry](https://sentry.io/welcome/) (including sourcemaps).
86 |
87 | You can enable the implementation by setting the keys you've generated as environment variables in your application.
88 |
89 | To get it fully operational requires:
90 |
91 | - generating keys in sentry
92 | - setting up environment variables
93 |
94 | The following keys are needed for the sample implementation:
95 |
96 | ```md
97 | - NEXT_IS_SERVER
98 | - NEXT_PUBLIC_SENTRY_SERVER_ROOT_DIR (Used to improve readability of the framepaths in the sourcemaps)
99 | - NODE_ENV (Sentry is only enabled when the `NODE_ENV` is production)
100 | - NEXT_PUBLIC_SENTRY_DSN (The DSN tells the SDK where to send the events)
101 | - NEXT_PUBLIC_COMMIT_SHA (Sets the release)
102 | ```
103 |
104 | To be able to upload the sourcemaps, you will need to add the following keys
105 |
106 | ```md
107 | - SENTRY_ORG
108 | - SENTRY_PROJECT
109 | - SENTRY_AUTH_TOKEN
110 | ```
111 |
112 | When all the Sentry configuration env variables are set, the Sentry webpack plugin gets pushed to the webpack plugins, to build and upload the source maps to sentry.
113 |
114 | This is an alternative to manually uploading the source maps and is disabled in development mode.
115 |
116 | ## Missing something?
117 |
118 | [Open an issue](https://github.com/storyofams/next-boilerplate/issues/new/choose) with your proposed change.
119 |
120 | ## Wanna help out?
121 |
122 | See [contributing.md](https://github.com/storyofams/next-boilerplate/blob/master/.github/CONTRIBUTING.md) to see how you can get started.
123 |
--------------------------------------------------------------------------------