├── .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 | 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 |