├── .prettierignore ├── .env ├── .babelrc ├── .eslintrc.json ├── preview.png ├── .env.production ├── .husky └── pre-commit ├── public └── static │ ├── favicon.png │ ├── og-image.png │ └── icons │ ├── sonarcloud-icon.jpg │ ├── testing-lib-icon.png │ ├── tailwindcss-icon.svg │ ├── font-awesome-icon.svg │ ├── eslint-icon.svg │ ├── nextjs-logo.svg │ ├── nextjs-icon.svg │ ├── typescript-icon.svg │ ├── github-icon.svg │ ├── prettier-icon.svg │ ├── jest-icon.svg │ ├── react-icon.svg │ ├── sass-icon.svg │ └── postcss-icon.svg ├── src ├── pages │ ├── _app.tsx │ ├── api │ │ └── date.ts │ ├── index.tsx │ └── docs.tsx ├── tsconfig.json ├── lambdas │ ├── getCurrentDateTime.ts │ └── __tests__ │ │ └── getCurrentDateTime.spec.ts ├── __tests__ │ ├── README.md │ ├── api │ │ ├── MockNextApiResponse.ts │ │ └── date.spec.ts │ └── pages │ │ ├── index.spec.tsx │ │ └── docs.spec.tsx ├── styles │ ├── _global.scss │ ├── _typography.scss │ ├── styles.scss │ ├── _buttons.scss │ └── _layout.scss ├── next-env.d.ts ├── lib │ └── apiGet.ts └── components │ ├── base │ ├── __tests__ │ │ └── Icon.spec.tsx │ ├── LinkButton.tsx │ └── Icon.tsx │ ├── layout │ ├── __tests__ │ │ └── Header.spec.tsx │ ├── Page.tsx │ ├── Head.tsx │ ├── Header.tsx │ └── Footer.tsx │ └── docs │ ├── __tests__ │ └── CodeBlocks.spec.tsx │ └── CodeBlock.tsx ├── .editorconfig ├── next-env.d.ts ├── postcss.config.js ├── .gitignore ├── .prettierrc ├── sonar-project.properties ├── tailwind.config.js ├── jest.config.js ├── next.config.js ├── .github └── workflows │ └── build.yml ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | out 3 | coverage 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_TITLE="Next.js Template" 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["next/babel"]] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/nextjs-template/HEAD/preview.png -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ROOT_URL="https://nextjs-template-juliensulpis.vercel.app" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn pre-commit 5 | -------------------------------------------------------------------------------- /public/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/nextjs-template/HEAD/public/static/favicon.png -------------------------------------------------------------------------------- /public/static/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/nextjs-template/HEAD/public/static/og-image.png -------------------------------------------------------------------------------- /public/static/icons/sonarcloud-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/nextjs-template/HEAD/public/static/icons/sonarcloud-icon.jpg -------------------------------------------------------------------------------- /public/static/icons/testing-lib-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsulpis/nextjs-template/HEAD/public/static/icons/testing-lib-icon.png -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import App from "next/app"; 2 | import "../styles/styles.scss"; 3 | import "@fortawesome/fontawesome-svg-core/styles.css"; 4 | 5 | export default App; 6 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /src/lambdas/getCurrentDateTime.ts: -------------------------------------------------------------------------------- 1 | export default function getCurrentDateTime(): string { 2 | return new Date(global.Date.now()).toISOString().replace(/T/, " ").replace(/\..+/, ""); 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/README.md: -------------------------------------------------------------------------------- 1 | The Next.js documentation suggests to avoid putting code that should not be deployed in the pages folder. 2 | 3 | So I moved the tests of the pages and the api functions in this separate folder. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | tab_width = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /src/styles/_global.scss: -------------------------------------------------------------------------------- 1 | // Global styling 2 | 3 | body { 4 | text-align: center; 5 | } 6 | 7 | html, 8 | body, 9 | #__next { 10 | height: 100%; 11 | } 12 | 13 | #__next { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/pages/api/date.ts: -------------------------------------------------------------------------------- 1 | import getCurrentDateTime from "lambdas/getCurrentDateTime"; 2 | import { NextApiResponse } from "next"; 3 | 4 | const dateApi = (_, res: NextApiResponse) => { 5 | const date = getCurrentDateTime(); 6 | 7 | res.status(200).json({ date }); 8 | }; 9 | 10 | export default dateApi; 11 | -------------------------------------------------------------------------------- /src/styles/_typography.scss: -------------------------------------------------------------------------------- 1 | body { 2 | @apply text-gray-700; 3 | } 4 | 5 | h2 { 6 | @apply font-semibold text-3xl; 7 | } 8 | 9 | h3 { 10 | @apply font-semibold text-2xl mt-6; 11 | } 12 | 13 | h4 { 14 | @apply font-semibold text-lg mt-3; 15 | } 16 | 17 | p, 18 | .p { 19 | @apply leading-relaxed opacity-90; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | // Processed by the Tailwind postcss plugin 2 | @tailwind base; 3 | @tailwind components; 4 | 5 | // Styles using Tailwind utilities 6 | @import "./buttons"; 7 | @import "./layout"; 8 | @import "./typography"; 9 | 10 | @tailwind utilities; 11 | 12 | // Styles not using Tailwind utilities 13 | @import "./global"; 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | "postcss-flexbugs-fixes": {}, 5 | "postcss-preset-env": { 6 | autoprefixer: { 7 | flexbox: "no-2009" 8 | }, 9 | stage: 3, 10 | features: { 11 | "custom-properties": false 12 | } 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/__tests__/api/MockNextApiResponse.ts: -------------------------------------------------------------------------------- 1 | export default class MockNextApiResponse { 2 | public statusCode: number; 3 | public body: object; 4 | 5 | public status(statusCode: number) { 6 | this.statusCode = statusCode; 7 | return this; 8 | } 9 | 10 | public json(body: object) { 11 | this.body = body; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply px-6 py-3 rounded shadow text-sm uppercase; 3 | 4 | &-primary { 5 | @apply bg-primary-500 text-white; 6 | 7 | &:hover { 8 | @apply bg-primary-600; 9 | } 10 | } 11 | 12 | &-gray { 13 | @apply bg-gray-500 text-white; 14 | 15 | &:hover { 16 | @apply bg-gray-600; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | **/build 11 | **/dist 12 | **/.next 13 | **/out 14 | **/.now 15 | 16 | # misc 17 | .DS_Store 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | .idea 22 | 23 | 24 | .vercel 25 | .env.local 26 | -------------------------------------------------------------------------------- /src/lib/apiGet.ts: -------------------------------------------------------------------------------- 1 | import fetch from "isomorphic-unfetch"; 2 | 3 | /** 4 | * Helper function to make API calls and testing pages easier 5 | * @param name: relative url of the API to call (will be appended to /api) 6 | */ 7 | export default async function apiGet(name: string): Promise { 8 | const ROOT_URL = window.location.origin; 9 | const API_URL = "/api"; 10 | 11 | const res = await fetch(ROOT_URL + API_URL + name); 12 | return res.json(); 13 | } 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 90, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": false, 14 | "tabWidth": 2, 15 | "trailingComma": "none", 16 | "useTabs": false, 17 | "endOfLine": "lf" 18 | } 19 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Project description 2 | sonar.organization=jsulpis-github 3 | sonar.projectKey=nextjs-template 4 | sonar.projectName=Next.js Template 5 | 6 | # Sources 7 | sonar.sources=src/components,src/lambdas,src/pages,src/styles 8 | sonar.profile=node 9 | 10 | # Code coverage 11 | sonar.coverage.exclusions=**/__tests__/*,**/*.spec.*,src/pages/_app.tsx,src/pages/_document.tsx 12 | sonar.dynamicAnalysis=reuseReports 13 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 14 | -------------------------------------------------------------------------------- /src/__tests__/pages/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import Index from "pages"; 3 | import * as router from "next/router"; 4 | 5 | jest.spyOn(router, "useRouter").mockReturnValue({} as any); 6 | 7 | describe("HomePage", () => { 8 | afterEach(() => { 9 | jest.clearAllMocks(); 10 | }); 11 | 12 | it("has a title", () => { 13 | const { container } = render(); 14 | expect(container.querySelector("h2").textContent).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/base/__tests__/Icon.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import Icon from "components/base/Icon"; 3 | 4 | describe("Icon", () => { 5 | it("should display an image with alt text and a title", () => { 6 | const { container, getByText } = render( 7 | 8 | ); 9 | 10 | const renderedImg = container.querySelector("img"); 11 | expect(renderedImg.alt).toBe("My Icon"); 12 | 13 | expect(getByText("My Icon")).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap-like containers which I find more responsive 2 | // (Tailwind leaves no or little space on the sides near each breakpoint) 3 | .container { 4 | width: 100%; 5 | @apply px-4; 6 | } 7 | 8 | @screen sm { 9 | .container { 10 | max-width: 600px; 11 | } 12 | } 13 | 14 | @screen md { 15 | .container { 16 | max-width: 720px; 17 | } 18 | } 19 | 20 | @screen lg { 21 | .container { 22 | max-width: 960px; 23 | } 24 | } 25 | 26 | @screen xl { 27 | .container { 28 | max-width: 1140px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { colors } = require("tailwindcss/defaultTheme"); 2 | 3 | module.exports = { 4 | mode: "jit", 5 | purge: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"], 6 | theme: { 7 | extend: { 8 | transitionProperty: { 9 | height: "height" 10 | }, 11 | colors: { 12 | primary: colors.blue 13 | }, 14 | opacity: { 15 | 90: ".9" 16 | } 17 | } 18 | }, 19 | corePlugins: { 20 | container: false // custom container class defined in styles/_compounds.scss 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/lambdas/__tests__/getCurrentDateTime.spec.ts: -------------------------------------------------------------------------------- 1 | import getCurrentDateTime from "lambdas/getCurrentDateTime"; 2 | 3 | describe("Date api", () => { 4 | let dateNowSpy; 5 | 6 | beforeAll(() => { 7 | // Lock Time 8 | dateNowSpy = jest 9 | .spyOn(global.Date, "now") 10 | .mockImplementation(() => +new Date("2019-09-14T12:13:14Z")); 11 | }); 12 | 13 | afterAll(() => { 14 | // Unlock Time 15 | dateNowSpy.mockRestore(); 16 | }); 17 | 18 | it("should return a date", () => { 19 | expect(getCurrentDateTime()).toEqual("2019-09-14 12:13:14"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testRegex: "(\\.|/)(test|spec)\\.(jsx?|js?|tsx?|ts?)$", 3 | transform: { "^.+\\.tsx?$": "babel-jest" }, 4 | testPathIgnorePatterns: [".next/", "node_modules/"], 5 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 6 | moduleDirectories: ["node_modules", "src"], 7 | testEnvironment: "jsdom", 8 | collectCoverageFrom: [ 9 | "src/components/**", 10 | "src/lambdas/**", 11 | "src/pages/**", 12 | "!src/pages/_app.tsx", 13 | "!src/pages/_document.tsx" 14 | ], 15 | moduleNameMapper: { 16 | "\\.(css|less|scss|sass)$": "identity-obj-proxy" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/layout/__tests__/Header.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, fireEvent } from "@testing-library/react"; 2 | import Header from "../Header"; 3 | import * as router from "next/router"; 4 | 5 | jest.spyOn(router, "useRouter").mockReturnValue({} as any); 6 | 7 | describe("Header", () => { 8 | it("should expand when clicking on the burger menu", () => { 9 | // Given 10 | const { container } = render(
); 11 | expect(container.querySelector("#list-mobile").classList).toContain("h-0"); 12 | 13 | // When 14 | fireEvent.click(container.querySelector("button")); 15 | 16 | // Then 17 | expect(container.querySelector("#list-mobile").classList).toContain("h-16"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/layout/Page.tsx: -------------------------------------------------------------------------------- 1 | import Head, { HeadProps } from "components/layout/Head"; 2 | import Header from "components/layout/Header"; 3 | import React, { PropsWithChildren, HTMLAttributes } from "react"; 4 | import Footer from "./Footer"; 5 | 6 | type PageProps = PropsWithChildren & 7 | HTMLAttributes & { 8 | mainClassName?: string; 9 | }; 10 | 11 | const Page = (props: PageProps) => ( 12 | <> 13 | 14 |
15 |
16 |
17 | {props.children} 18 |
19 |
20 |