├── .prettierignore ├── .eslintignore ├── .yarnrc ├── src ├── styles │ ├── normalise.scss │ └── tailwind.scss ├── modules │ ├── index.js │ └── Landing │ │ ├── index.js │ │ ├── Landing.js │ │ └── Landing.styled.js ├── components │ ├── Loader │ │ ├── index.js │ │ ├── Loader.js │ │ └── Loader.styled.js │ ├── Favicon │ │ ├── index.js │ │ └── Favicon.js │ ├── Layout │ │ ├── Common │ │ │ ├── index.js │ │ │ ├── Common.styled.js │ │ │ └── Common.js │ │ ├── Landing │ │ │ ├── index.js │ │ │ ├── Landing.styled.js │ │ │ └── Landing.js │ │ ├── Outlet │ │ │ ├── index.js │ │ │ ├── Outlet.styled.js │ │ │ └── Outlet.js │ │ ├── NotFound │ │ │ ├── index.js │ │ │ ├── NotFound.styled.js │ │ │ └── NotFound.js │ │ └── index.js │ └── ErrorFallback │ │ ├── index.js │ │ ├── ErrorFallback.styled.js │ │ └── ErrorFallback.js ├── config.js ├── assets │ ├── fonts │ │ ├── inter-black.woff │ │ ├── inter-bold.woff │ │ ├── inter-bold.woff2 │ │ ├── inter-light.woff │ │ ├── inter-thin.woff │ │ ├── inter-thin.woff2 │ │ ├── inter.var.woff2 │ │ ├── inter-black.woff2 │ │ ├── inter-light.woff2 │ │ ├── inter-medium.woff │ │ ├── inter-medium.woff2 │ │ ├── inter-regular.woff │ │ ├── inter-regular.woff2 │ │ ├── inter-semibold.woff │ │ ├── inter-extrabold.woff │ │ ├── inter-extrabold.woff2 │ │ ├── inter-extralight.woff │ │ ├── inter-roman.var.woff2 │ │ ├── inter-semibold.woff2 │ │ ├── inter-extralight.woff2 │ │ └── stylesheet.css │ └── images │ │ └── react.svg ├── utils │ ├── common.js │ ├── queryClient.js │ ├── parseTokenInfo.js │ └── axiosClient.js ├── hooks │ ├── useDocumentTitle.js │ ├── useQueryState.js │ ├── useOutsideClick.js │ ├── useDebounce.js │ ├── useAutoScroll.js │ ├── useOnScreen.js │ ├── withErrorBoundary.js │ ├── useScrollToTop.js │ └── useLocalStorage.js ├── App.js ├── GlobalStyle.js ├── theme │ └── index.js ├── index.js └── router │ └── ProtectedRoute.js ├── .env.dev.example ├── .env.prod.example ├── .vscode ├── settings.json └── extensions.json ├── vercel.json ├── tailwind.config.js ├── .editorconfig ├── postcss.config.js ├── .jscpd.json ├── .prettierrc.js ├── public ├── images │ └── react.svg └── index.html ├── .stylelintrc.js ├── .aws └── buildspec-files │ └── buildspec.yml ├── .cspell.json ├── jsconfig.json ├── .github └── workflows │ ├── npm-publish-github-packages.yml │ └── lint.yml ├── LICENSE ├── webpack.config.dev.js ├── docs └── README.md ├── .babelrc ├── webpack.config.prod.js ├── README.md ├── .gitignore ├── webpack.config.common.js ├── package.json └── .eslintrc.js /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .prettierrc.js 3 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry: https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /src/styles/normalise.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | export { default as Landing } from './Landing'; 2 | -------------------------------------------------------------------------------- /.env.dev.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | 3 | APP_API_URL= 4 | APP_ASSET_URL= 5 | -------------------------------------------------------------------------------- /src/components/Loader/index.js: -------------------------------------------------------------------------------- 1 | import Loader from './Loader'; 2 | 3 | export default Loader; 4 | -------------------------------------------------------------------------------- /src/modules/Landing/index.js: -------------------------------------------------------------------------------- 1 | import Landing from './Landing'; 2 | 3 | export default Landing; 4 | -------------------------------------------------------------------------------- /src/components/Favicon/index.js: -------------------------------------------------------------------------------- 1 | import Favicon from './Favicon'; 2 | 3 | export default Favicon; 4 | -------------------------------------------------------------------------------- /src/components/Layout/Common/index.js: -------------------------------------------------------------------------------- 1 | import Common from './Common'; 2 | 3 | export default Common; 4 | -------------------------------------------------------------------------------- /src/components/Layout/Landing/index.js: -------------------------------------------------------------------------------- 1 | import Landing from './Landing'; 2 | 3 | export default Landing; 4 | -------------------------------------------------------------------------------- /src/components/Layout/Outlet/index.js: -------------------------------------------------------------------------------- 1 | import Outlet from './Outlet'; 2 | 3 | export default Outlet; 4 | -------------------------------------------------------------------------------- /src/components/Layout/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import NotFound from './NotFound'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const { APP_API_URL } = process.env; 2 | 3 | export const { APP_ASSET_URL } = process.env; 4 | -------------------------------------------------------------------------------- /src/assets/fonts/inter-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-black.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-bold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-light.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-thin.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-thin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter.var.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-black.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-light.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-medium.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-regular.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-semibold.woff -------------------------------------------------------------------------------- /src/components/ErrorFallback/index.js: -------------------------------------------------------------------------------- 1 | import ErrorFallback from './ErrorFallback'; 2 | 3 | export default ErrorFallback; 4 | -------------------------------------------------------------------------------- /src/assets/fonts/inter-extrabold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-extrabold.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-extrabold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-extrabold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-extralight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-extralight.woff -------------------------------------------------------------------------------- /src/assets/fonts/inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-roman.var.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-semibold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/inter-extralight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/react-starter/main/src/assets/fonts/inter-extralight.woff2 -------------------------------------------------------------------------------- /src/components/Layout/Landing/Landing.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | margin: 0; 5 | padding: 0; 6 | `; 7 | -------------------------------------------------------------------------------- /src/styles/tailwind.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply text-base; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.env.prod.example: -------------------------------------------------------------------------------- 1 | APP_API_URL= 2 | APP_ASSET_URL= 3 | 4 | COMPRESS_ASSETS= 5 | 6 | DEPLOY_TO_S3= 7 | AWS_ACCESS_KEY_ID= 8 | AWS_SECRET_ACCESS_KEY= 9 | FRONTEND_BUCKET_NAME= 10 | 11 | -------------------------------------------------------------------------------- /src/components/Layout/Common/Common.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | width: 100%; 5 | height: 100%; 6 | display: grid; 7 | `; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.codeActionsOnSave.mode": "all", 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": "explicit", 5 | "source.fixAll": "explicit" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Container } from './Loader.styled'; 4 | 5 | const Loader = () => Logo; 6 | 7 | export default Loader; 8 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "buildCommand": "yarn build:prod", 4 | "installCommand": "yarn install --frozen-lockfile", 5 | "outputDirectory": "dist" 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./dist/*.{html, js}', './src/**/*.js'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/Layout/NotFound/NotFound.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledContainer = styled.div` 4 | display: grid; 5 | place-items: center; 6 | height: 100vh; 7 | `; 8 | -------------------------------------------------------------------------------- /src/utils/common.js: -------------------------------------------------------------------------------- 1 | import { APP_ASSET_URL } from '#/config'; 2 | 3 | export const getAssetURL = (filename) => { 4 | if (/^(http|\/)/.test(filename)) return filename; 5 | 6 | return `${APP_ASSET_URL}/v1/files/${filename}`; 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/Layout/Outlet/Outlet.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | padding: 0; 5 | display: flex; 6 | 7 | &, 8 | & > div { 9 | min-height: calc(100vh); 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import Common from './Common'; 2 | import Landing from './Landing'; 3 | import NotFound from './NotFound'; 4 | import Outlet from './Outlet'; 5 | 6 | export default { 7 | Common, 8 | Outlet, 9 | Landing, 10 | NotFound, 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | 12 | [{mega-linter.yml}] 13 | max_line_length = off 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('postcss').Postcss} 3 | */ 4 | module.exports = { 5 | plugins: { 6 | 'postcss-preset-env': {}, 7 | tailwindcss: {}, 8 | autoprefixer: {}, 9 | ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.jscpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "threshold": 0, 3 | "reporters": ["html", "markdown"], 4 | "ignore": [ 5 | "**/node_modules/**", 6 | "**/.git/**", 7 | "**/.rbenv/**", 8 | "**/.venv/**", 9 | "**/*cache*/**", 10 | "**/.github/**", 11 | "**/.idea/**", 12 | "**/report/**", 13 | "**/*.svg" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("prettier").Config} 3 | */ 4 | const config = { 5 | printWidth: 120, 6 | singleQuote: true, 7 | trailingComma: 'es5', 8 | semi: true, 9 | endOfLine: 'lf', 10 | jsxSingleQuote: false, 11 | tabWidth: 2, 12 | arrowParens: 'always', 13 | }; 14 | 15 | module.exports = config; 16 | -------------------------------------------------------------------------------- /src/components/ErrorFallback/ErrorFallback.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | flex-direction: column; 7 | justify-content: center; 8 | text-align: center; 9 | max-width: 217px; 10 | gap: 1rem; 11 | margin: auto; 12 | `; 13 | -------------------------------------------------------------------------------- /src/components/Layout/Common/Common.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Outlet from '../Outlet'; 4 | 5 | import { Container } from './Common.styled'; 6 | 7 | const Common = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default Common; 16 | -------------------------------------------------------------------------------- /src/components/Layout/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { StyledContainer } from './NotFound.styled'; 4 | 5 | const NotFound = () => { 6 | return ( 7 | 8 |
9 |

404

10 |

Not found

11 |
12 |
13 | ); 14 | }; 15 | 16 | export default NotFound; 17 | -------------------------------------------------------------------------------- /src/utils/queryClient.js: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | /** 4 | * @typedef {import('@tanstack/react-query').QueryClient} queryClient 5 | * @returns {queryClient} 6 | */ 7 | const queryClient = new QueryClient({ 8 | defaultOptions: { 9 | queries: { 10 | useErrorBoundary: true, 11 | }, 12 | }, 13 | }); 14 | 15 | export default queryClient; 16 | -------------------------------------------------------------------------------- /public/images/react.svg: -------------------------------------------------------------------------------- 1 | 2 | React Logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/images/react.svg: -------------------------------------------------------------------------------- 1 | 2 | React Logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/modules/Landing/Landing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { StyledContainer } from './Landing.styled'; 4 | 5 | import ReactLogo from '#/assets/images/react.svg?react'; 6 | import WithErrorBoundary from '#/hooks/withErrorBoundary'; 7 | 8 | const Landing = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default WithErrorBoundary(Landing); 17 | -------------------------------------------------------------------------------- /src/utils/parseTokenInfo.js: -------------------------------------------------------------------------------- 1 | import JWTDecode from 'jwt-decode'; 2 | 3 | /** 4 | * Parse token and return encoded Object 5 | * 6 | * @param {string} token - Valid token string 7 | * @return {object} Token Information 8 | */ 9 | const parseTokenInfo = (token) => { 10 | try { 11 | return JWTDecode(token); 12 | } catch (error) { 13 | console.error(error); 14 | 15 | return {}; 16 | } 17 | }; 18 | 19 | export default parseTokenInfo; 20 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('stylelint').Config} */ 2 | module.exports = { 3 | processors: ["stylelint-processor-styled-components"], 4 | extends: ["stylelint-config-recommended"], 5 | rules: { 6 | 'at-rule-no-unknown': [ 7 | true, 8 | { 9 | ignoreAtRules: [ 10 | 'tailwind', 11 | 'apply', 12 | 'variants', 13 | 'responsive', 14 | 'screen', 15 | ], 16 | }, 17 | ], 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Layout/Outlet/Outlet.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | 3 | import { Outlet } from 'react-router-dom'; 4 | 5 | import { Container } from './Outlet.styled'; 6 | 7 | import Loader from '#/components/Loader'; 8 | 9 | const OutletContainer = () => { 10 | return ( 11 | }> 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default OutletContainer; 20 | -------------------------------------------------------------------------------- /.aws/buildspec-files/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | nodejs: 18 7 | commands: 8 | - n 20 9 | - echo installing dependencies for env $NODE_ENV... 10 | - yarn install 11 | build: 12 | commands: 13 | - echo building project 14 | - yarn build:prod 15 | artifacts: 16 | files: 17 | - '**/*' 18 | discard-paths: no 19 | base-directory: dist 20 | cache: 21 | paths: 22 | - 'node_modules/**/*' 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "formulahendry.auto-close-tag", 4 | "aaron-bond.better-comments", 5 | "streetsidesoftware.code-spell-checker", 6 | "usernamehw.errorlens", 7 | "dbaeumer.vscode-eslint", 8 | "esbenp.prettier-vscode", 9 | "bradlc.vscode-tailwindcss" 10 | ], 11 | "unwantedRecommendations": [ 12 | "mgmcdermott.vscode-language-babel", 13 | "oouo-diogo-perdigao.docthis", 14 | "stylelint.vscode-stylelint" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Layout/Landing/Landing.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | 3 | import { Outlet } from 'react-router-dom'; 4 | 5 | import { Container } from './Landing.styled'; 6 | 7 | import Loader from '#/components/Loader'; 8 | 9 | const Landing = () => { 10 | return ( 11 | 12 | 13 | 14 |
Footer
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Landing; 21 | -------------------------------------------------------------------------------- /src/modules/Landing/Landing.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const StyledContainer = styled.div` 4 | margin: 0; 5 | padding: 0; 6 | 7 | width: 100%; 8 | height: 100vh; 9 | 10 | display: grid; 11 | place-items: center; 12 | 13 | & > svg { 14 | animation: rotate 5s linear infinite; 15 | } 16 | 17 | @keyframes rotate { 18 | 0% { 19 | transform: rotate(0); 20 | } 21 | 22 | 100% { 23 | transform: rotate(360deg); 24 | } 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/hooks/useDocumentTitle.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 4 | * Set document title 5 | * 6 | * @param {string} [title=''] 7 | * @param {*} [trigger=null] 8 | */ 9 | const useDocumentTitle = (title = '', trigger = null) => { 10 | const setNewTitle = (t) => { 11 | document.title = [t, 'PROJECT_NAME'].filter((part) => part.length > 0).join(' | '); 12 | }; 13 | 14 | useEffect(() => { 15 | setNewTitle(title); 16 | }, [trigger, title]); 17 | }; 18 | 19 | export default useDocumentTitle; 20 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": [ 3 | "**/node_modules/**", 4 | "**/vscode-extension/**", 5 | "**/.git/**", 6 | "**/.pnpm-lock.json", 7 | ".vscode", 8 | "megalinter", 9 | "package-lock.json", 10 | "report", 11 | "**/buildspec-files/**" 12 | ], 13 | "language": "en", 14 | "noConfigSearch": true, 15 | "words": [ 16 | "Aakash", 17 | "aakashgajjar", 18 | "akash", 19 | "gajjar", 20 | "megalinter", 21 | "oxsecurity", 22 | "pmmmwh", 23 | "sigmacomputing", 24 | "stylelint", 25 | "svgr", 26 | "tanstack" 27 | ], 28 | "version": "0.2" 29 | } 30 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | 3 | import { Route, Routes, useLocation } from 'react-router-dom'; 4 | 5 | import Layout from '#/components/Layout'; 6 | import { Landing } from '#/modules'; 7 | 8 | const App = () => { 9 | const location = useLocation(); 10 | 11 | return ( 12 | Loading}> 13 | 14 | }> 15 | } /> 16 | 17 | 18 | } /> 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.styled.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | @keyframes rotate { 12 | 0% { 13 | transform: rotate(0deg); 14 | } 15 | 100% { 16 | transform: rotate(360deg); 17 | } 18 | } 19 | 20 | svg { 21 | width: clamp(150px, 50vw, 400px); 22 | height: 200px; 23 | 24 | circle { 25 | animation: rotate 10s infinite linear; 26 | 27 | &:nth-child(2n + 1) { 28 | animation: rotate 10s infinite linear reverse; 29 | } 30 | } 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /src/components/Favicon/Favicon.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const Favicon = () => { 4 | useEffect(() => { 5 | const query = window.matchMedia('(prefers-color-scheme: dark)'); 6 | const favicon = document.querySelector('link[rel~="icon"]'); 7 | 8 | const handleMatch = (isMatch) => { 9 | // we force refresh favicon using version specifiers 10 | const value = isMatch ? '/images/favicon.png' : '/images/favicon.png'; 11 | favicon.href = `${value}?v=v${Date.now()}`; 12 | }; 13 | 14 | query.onchange = (evt) => { 15 | handleMatch(evt.matches); 16 | }; 17 | 18 | handleMatch(query.matches); 19 | }, []); 20 | 21 | return null; 22 | }; 23 | 24 | export default Favicon; 25 | -------------------------------------------------------------------------------- /src/GlobalStyle.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | 5 | * { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | html, body, #root { 11 | width: 100%; 12 | min-height: 100vh; 13 | font-family: ${({ theme }) => theme.fontFamily.regular}; 14 | overflow-x: hidden; 15 | background: white; 16 | color: black; 17 | } 18 | 19 | h1, h2, h3, h4, h5, h6, p { 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | // Default text selection color 25 | ::selection { 26 | color: white !important; 27 | background: black !important; 28 | } 29 | 30 | ul li { 31 | list-style: none; 32 | } 33 | 34 | input { 35 | accent-color: black; 36 | } 37 | `; 38 | 39 | export default GlobalStyle; 40 | -------------------------------------------------------------------------------- /src/components/ErrorFallback/ErrorFallback.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PropTypes from 'prop-types'; 4 | 5 | import { Container } from './ErrorFallback.styled'; 6 | 7 | const ErrorFallback = ({ error, resetErrorBoundary }) => { 8 | return ( 9 | 10 |
Error Image
11 |
Something went wrong!
12 |
{error?.message}
13 | 16 |
17 | ); 18 | }; 19 | 20 | ErrorFallback.propTypes = { 21 | error: PropTypes.shape({ message: PropTypes.string }).isRequired, 22 | resetErrorBoundary: PropTypes.func.isRequired, 23 | }; 24 | 25 | export default ErrorFallback; 26 | -------------------------------------------------------------------------------- /src/hooks/useQueryState.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from 'react'; 2 | 3 | import { useSearchParams } from 'react-router-dom'; 4 | 5 | /** 6 | * Returns a stateful query value, and a function to update it. 7 | * 8 | * @param {object} initialState - Initial State 9 | */ 10 | const useQueryState = (initialState) => { 11 | const [searchParams, setSearchParams] = useSearchParams(); 12 | 13 | const [queryState, setQueryState] = useReducer((state, action) => ({ ...state, ...action }), { 14 | ...initialState, 15 | ...Object.fromEntries([...searchParams]), 16 | }); 17 | 18 | useEffect(() => { 19 | setSearchParams(queryState); 20 | }, [queryState, setSearchParams]); 21 | 22 | return [queryState, setQueryState]; 23 | }; 24 | 25 | export default useQueryState; 26 | -------------------------------------------------------------------------------- /src/hooks/useOutsideClick.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 4 | * Trigger function call when mouse click is outside the ref element. 5 | * 6 | * @param {{current:import('react').ReactHTMLElement}} ref 7 | * @param {function} handler 8 | */ 9 | export const useOutsideClick = (ref, handler) => { 10 | useEffect(() => { 11 | const listener = (event) => { 12 | if (!ref.current || ref.current.contains(event.target)) { 13 | return; 14 | } 15 | 16 | handler(event); 17 | }; 18 | 19 | document.addEventListener('mousedown', listener); 20 | document.addEventListener('touchstart', listener); 21 | 22 | return () => { 23 | document.removeEventListener('mousedown', listener); 24 | document.removeEventListener('touchstart', listener); 25 | }; 26 | }, [ref, handler]); 27 | }; 28 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "moduleResolution": "Node", 6 | "checkJs": false, 7 | "jsx": "react", 8 | "baseUrl": ".", 9 | "paths": { 10 | "#/*": ["./src/*"], 11 | "#/assets/*": ["./src/assets/*"], 12 | "#/components/*": ["./src/components/*"], 13 | "#/constants/*": ["./src/constants/*"], 14 | "#/hooks/*": ["./src/hooks/*"], 15 | "#/modules/*": ["./src/modules/*"], 16 | "#/router/*": ["./src/router/*"], 17 | "#/services/*": ["./src/services/*"], 18 | "#/context/*": ["./src/context/*"], 19 | "#/styles/*": ["./src/styles/*"], 20 | "#/utils/*": ["./src/utils/*"], 21 | "#/queries/*": ["./src/queries/*"], 22 | "#/theme/*": ["./src/theme/*"] 23 | } 24 | }, 25 | "include": ["src/**/*"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const useDebounce = (value, delay) => { 4 | // State and setters for debounced value 5 | const [debouncedValue, setDebouncedValue] = useState(value); 6 | useEffect( 7 | () => { 8 | // Update debounced value after delay 9 | const handler = setTimeout(() => { 10 | setDebouncedValue(value); 11 | }, delay); 12 | 13 | // Cancel the timeout if value changes (also on delay change or unmount) 14 | // This is how we prevent debounced value from updating if value is 15 | // changed within the delay period. Timeout gets cleared and restarted. 16 | return () => { 17 | clearTimeout(handler); 18 | }; 19 | }, 20 | [value, delay] // Only re-call effect if value or delay changes 21 | ); 22 | 23 | return debouncedValue; 24 | }; 25 | 26 | export default useDebounce; 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | React Starter 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/hooks/useAutoScroll.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | /** 4 | * Auto scroll on change in target element 5 | * 6 | * @param {{current:import('react').ReactHTMLElement}} target 7 | * @param {{minHeight: number}} [options={ minHeight: 700 }] 8 | */ 9 | const useAutoScroll = (target, options = { minHeight: 700 }) => { 10 | useEffect(() => { 11 | if (target.current) { 12 | const { height } = target.current.getBoundingClientRect(); 13 | const translateHeight = height - options.minHeight; 14 | target.current.parentElement.setAttribute( 15 | 'style', 16 | ` 17 | max-height: ${options.minHeight}px; 18 | overflow: hidden; 19 | ` 20 | ); 21 | target.current.setAttribute( 22 | 'style', 23 | ` 24 | transition: transform 2000ms 4000ms ease-in-out; 25 | transform: translate(0, ${translateHeight}px) 26 | ` 27 | ); 28 | } 29 | }, [target, options.minHeight]); 30 | }; 31 | 32 | export default useAutoScroll; 33 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | const theme = { 2 | breakpoints: { 3 | sm: '40em', 4 | md: '52em', 5 | lg: '64em', 6 | xl: '80em', 7 | }, 8 | fontFamily: { 9 | thin: 'inter_thin', 10 | extralight: 'inter_extralight', 11 | light: 'inter_light', 12 | regular: 'inter_regular', 13 | medium: 'inter_medium', 14 | semibold: 'inter_semibold', 15 | bold: 'inter_bold', 16 | extrabold: 'inter_extrabold', 17 | black: 'inter_black', 18 | }, 19 | fontWeight: [100, 200, 300, 400, 500, 600, 700, 800, 900], 20 | fontSizes: [8, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 96, 128], 21 | lineHeight: [8, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 96, 128], 22 | space: [0, 4, 8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 256], 23 | colors: { 24 | black: '#000000', 25 | white: '#FFFFFF', 26 | goldenrod: '#D3A718', 27 | tealBlue: '#3F7B83', 28 | darkJungleGreen: '#1D2025', 29 | graniteGray: '#606060', 30 | }, 31 | }; 32 | 33 | export default theme; 34 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-github-packages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-gpr: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: 16 32 | registry-url: https://npm.pkg.github.com/ 33 | - run: npm ci 34 | - run: npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aakash Gajjar (hello@aakashgajjar.dev) 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 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [20.x] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - uses: actions/cache@v3 24 | with: 25 | path: '**/node_modules' 26 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 27 | - name: Fetch the base branch 28 | run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }} 29 | - name: Install dependencies 30 | run: | 31 | yarn --frozen-lockfile --production=false 32 | - name: Run eslint on changed files 33 | uses: tj-actions/eslint-changed-files@v14 34 | with: 35 | config_path: '.eslintrc.js' 36 | extra_args: '--max-warnings=0' 37 | file_extensions: | 38 | **/*.js 39 | -------------------------------------------------------------------------------- /src/hooks/useOnScreen.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | /** 4 | * Hook to trigger some event when element is visible on screen 5 | * 6 | * @param {*} ref 7 | * @param {string} [rootMargin='0px'] 8 | * @return {boolean} - true if ref element is visible 9 | * 10 | * See: https://usehooks.com/useOnScreen/ 11 | */ 12 | const useOnScreen = (ref, rootMargin = '0px') => { 13 | // State and setter for storing whether element is visible 14 | const [isIntersecting, setIsIntersecting] = useState(false); 15 | 16 | useEffect(() => { 17 | const { current } = ref; 18 | const observer = new IntersectionObserver( 19 | ([entry]) => { 20 | // Update our state when observer callback fires 21 | setIsIntersecting(entry.isIntersecting); 22 | }, 23 | { 24 | rootMargin, 25 | } 26 | ); 27 | 28 | if (current) { 29 | observer.observe(current); 30 | } 31 | 32 | return () => { 33 | if (current) { 34 | observer.unobserve(current); 35 | } 36 | }; 37 | }, [ref, rootMargin]); // Empty array ensures that effect is only run on mount and unmount 38 | 39 | return isIntersecting; 40 | }; 41 | 42 | export default useOnScreen; 43 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { QueryClientProvider } from '@tanstack/react-query'; 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 5 | import { createRoot } from 'react-dom/client'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | import { ThemeProvider } from 'styled-components'; 8 | 9 | import './styles/normalise.scss'; 10 | 11 | import App from './App'; 12 | import GlobalStyle from './GlobalStyle'; 13 | import theme from './theme'; 14 | 15 | import '#/assets/fonts/stylesheet.css'; 16 | import Favicon from '#/components/Favicon'; 17 | import queryClient from '#/utils/queryClient'; 18 | 19 | const container = document.getElementById('root'); 20 | const root = createRoot(container, {}); 21 | root.render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Hello world

31 |
32 |
33 |
34 |
35 | ); 36 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | require('dotenv').config({ path: './.env' }); 5 | const StylelintPlugin = require('stylelint-webpack-plugin'); 6 | const webpack = require('webpack'); 7 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 8 | const { merge } = require('webpack-merge'); 9 | 10 | const commonConfig = require('./webpack.config.common'); 11 | 12 | module.exports = (_env) => 13 | merge(commonConfig, { 14 | mode: 'development', 15 | devtool: 'inline-source-map', 16 | devServer: { 17 | allowedHosts: 'all', 18 | static: { 19 | directory: path.resolve(__dirname, 'public'), 20 | publicPath: `/`, 21 | }, 22 | port: 6600, 23 | historyApiFallback: true, 24 | }, 25 | plugins: [ 26 | new StylelintPlugin(), 27 | new webpack.HotModuleReplacementPlugin(), 28 | new ReactRefreshWebpackPlugin(), 29 | new BundleAnalyzerPlugin({ 30 | openAnalyzer: false, 31 | }), 32 | new webpack.DefinePlugin({ 33 | process: { 34 | env: JSON.stringify(process.env), 35 | }, 36 | }), 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Using Error Boundary 2 | 3 | Error handling is implemented using `ErrorBoundary` component provided from `react-error-boundary` 4 | package, to catch error we simple need to wrap our component and provide `fallbackRender` component 5 | to display error in `production`. 6 | 7 | ```jsx 8 | 9 | {}} 11 | isAddAddressFormOpen={isAddAddressFormOpen} 12 | setIsAddAddressFormOpen={setIsAddAddressFormOpen} 13 | /> 14 | 15 | ``` 16 | 17 | We can reset error state for any component if it uses `react-query` using 18 | `QueryErrorResetBoundary`. For this we wrap our component that uses `react-query` 19 | in its children and call `reset` inside our `ErrorBoundary`. 20 | 21 | For this to work we need to set `useErrorBoundary: true` in `react-query` options. 22 | 23 | ```jsx 24 | 25 | {({ reset }) => ( 26 | 27 | {}} 29 | isAddAddressFormOpen={isAddAddressFormOpen} 30 | setIsAddAddressFormOpen={setIsAddAddressFormOpen} 31 | /> 32 | 33 | )} 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /src/router/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PropType from 'prop-types'; 4 | import { Navigate, Outlet } from 'react-router-dom'; 5 | 6 | import useLocalStorage from '#/hooks/useLocalStorage'; 7 | import WithErrorBoundary from '#/hooks/withErrorBoundary'; 8 | 9 | const ProtectedRoute = ({ fallbackRoute, ...rest }) => { 10 | // Protect routes when user is logged in or when it has valid tokens 11 | // in the url query string. We want to protect `reset-password` route 12 | // when user is resetting their account password. 13 | const [token] = useLocalStorage('token', null); 14 | 15 | if (!token) { 16 | return ( 17 | 26 | ); 27 | } 28 | 29 | // Instead of returning child element we return Outlet so we can 30 | // render child routes. 31 | return ; 32 | }; 33 | 34 | ProtectedRoute.propTypes = { 35 | fallbackRoute: PropType.string, 36 | element: PropType.node, 37 | }; 38 | 39 | ProtectedRoute.defaultProps = { 40 | fallbackRoute: '/login', 41 | element: null, 42 | }; 43 | 44 | export default WithErrorBoundary(ProtectedRoute); 45 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | [ 8 | "@sigmacomputing/babel-plugin-lodash" 9 | ], 10 | [ 11 | "module-resolver", 12 | { 13 | "root": [ 14 | "./src" 15 | ], 16 | "alias": { 17 | "#/assets": "./src/assets/", 18 | "#/components": "./src/components/", 19 | "#/constants": "./src/constants/", 20 | "#/context": "./src/context", 21 | "#/hooks": "./src/hooks/", 22 | "#/modules": "./src/modules/", 23 | "#/router": "./src/router/", 24 | "#/services": "./src/services/", 25 | "#/styles": "./src/styles/", 26 | "#/utils": "./src/utils/", 27 | "#/config": "./src/config.js", 28 | "#/queries": "./src/queries/", 29 | "#/theme": "./src/theme/", 30 | "#/": "./src/" 31 | }, 32 | "extensions": [ 33 | ".js", 34 | ".jsx", 35 | ".json", 36 | ".svg", 37 | ".jpg", 38 | ".png" 39 | ] 40 | } 41 | ], 42 | "@babel/plugin-proposal-class-properties", 43 | [ 44 | "babel-plugin-styled-components", 45 | { 46 | "ssr": true 47 | } 48 | ] 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/axiosClient.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { APP_API_URL } from '#/config'; 4 | 5 | const EXCLUDE_REDIRECT_PATHS = ['/login']; 6 | 7 | const axiosClient = axios.create({ 8 | baseURL: `${APP_API_URL}`, 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | }); 13 | 14 | axiosClient.interceptors.request.use((config) => { 15 | const token = localStorage.getItem('token'); 16 | 17 | if (token && token !== 'undefined') { 18 | // eslint-disable-next-line no-param-reassign 19 | config.headers.Authorization = JSON.parse(token); 20 | } 21 | 22 | return config; 23 | }); 24 | 25 | axiosClient.interceptors.response.use( 26 | (response) => { 27 | const { 28 | url, 29 | status, 30 | data: { token }, 31 | } = response; 32 | 33 | if (url === `${APP_API_URL}` && status === 200) { 34 | localStorage.setItem('token', JSON.stringify(token)); 35 | } 36 | 37 | return response; 38 | }, 39 | (error) => { 40 | const { 41 | response: { status }, 42 | } = error; 43 | 44 | if (status === 401 && !EXCLUDE_REDIRECT_PATHS.includes(window.location.pathname)) { 45 | window.localStorage.clear(); 46 | window.location.href = '/'; 47 | } 48 | 49 | return Promise.reject(error); 50 | } 51 | ); 52 | 53 | export { axiosClient }; 54 | -------------------------------------------------------------------------------- /src/hooks/withErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | 3 | import { QueryErrorResetBoundary } from '@tanstack/react-query'; 4 | import PropTypes from 'prop-types'; 5 | import { ErrorBoundary } from 'react-error-boundary'; 6 | 7 | import ErrorFallback from '#/components/ErrorFallback'; 8 | 9 | /** 10 | * Wrap component using `react-error-boundary` 11 | * 12 | * @typedef {import('react').ReactNode} Component 13 | * 14 | * @param {Component} Component 15 | * @param {*} ErrorFallbackComponent 16 | * @return {Component} 17 | */ 18 | const WithErrorBoundary = (Component, ErrorFallbackComponent) => { 19 | const Closure = (props) => { 20 | return ( 21 | Loading}> 22 | 23 | {({ reset }) => ( 24 | 25 | 26 | 27 | )} 28 | 29 | 30 | ); 31 | }; 32 | Closure.displayName = 'ComponentWithErrorBoundary'; 33 | 34 | return Closure; 35 | }; 36 | 37 | WithErrorBoundary.defaultProps = { 38 | ErrorFallbackComponent: ErrorFallback, 39 | }; 40 | 41 | WithErrorBoundary.propTypes = { 42 | Component: PropTypes.node.isRequired, 43 | ErrorFallbackComponent: PropTypes.node, 44 | }; 45 | 46 | export default WithErrorBoundary; 47 | -------------------------------------------------------------------------------- /src/hooks/useScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | import PropTypes from 'prop-types'; 4 | 5 | /** 6 | * Scroll element into view or scroll to Top 7 | * 8 | * @param {object} options - Scroll options 9 | * 10 | * @param {string} options.behavior - Scroll options `auto` or `smooth`. 11 | * @param {string} options.trigger - reference value to trigger scroll effect 12 | * @param {boolean} options.scrollInto - `true` for scroll into-view `false` 13 | * otherwise 14 | * 15 | * @return {response} useQuery response 16 | */ 17 | const ScrollToTop = ({ behavior, scrollInto = false, trigger = null }) => { 18 | const injectedDiv = useRef(); 19 | 20 | useEffect(() => { 21 | try { 22 | if (scrollInto && injectedDiv.current) { 23 | injectedDiv.current.scrollIntoView({ behavior: behavior || 'smooth' }); 24 | } else { 25 | window.scroll({ 26 | top: 0, 27 | left: 0, 28 | behavior: behavior || 'smooth', 29 | }); 30 | } 31 | } catch (err) { 32 | window.scrollTo(0, 0); 33 | } 34 | 35 | return () => null; 36 | }, [behavior, scrollInto, trigger]); 37 | 38 | return scrollInto ?
: null; 39 | }; 40 | 41 | ScrollToTop.propTypes = { 42 | behavior: PropTypes.string, 43 | scrollInto: PropTypes.bool, 44 | trigger: PropTypes.node, 45 | }; 46 | 47 | ScrollToTop.defaultProps = { 48 | behavior: 'smooth', 49 | scrollInto: false, 50 | trigger: null, 51 | }; 52 | 53 | export { ScrollToTop }; 54 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | /** 4 | * React Hook for interacting with Browser's Local Storage 5 | * 6 | * @param {string} keyName - localstorage `key` name 7 | * @param {string|object} initialValue - fallback value to use 8 | * @return {array} - [value, setValue] 9 | * 10 | * See: https://web.archive.org/web/20220503083827/https://usehooks.com/useLocalStorage/ 11 | */ 12 | const useLocalStorage = (keyName, initialValue) => { 13 | const [storedValue, setStoredValue] = useState(() => { 14 | if (typeof window === 'undefined') { 15 | return initialValue; 16 | } 17 | 18 | try { 19 | // Get from local storage by keyName 20 | const item = window.localStorage.getItem(keyName); 21 | 22 | // Parse stored json or if none return initialvalue 23 | return item || initialValue; 24 | } catch (err) { 25 | // If errors also return initialvalue 26 | // TODO: Handle these error cases 27 | // eslint-disable-next-line no-console 28 | console.error(err); 29 | 30 | return initialValue; 31 | } 32 | }); 33 | 34 | // Return a wrapped version of useState's setter function that 35 | // persists the new value to localStorage 36 | const setValue = (value) => { 37 | try { 38 | const valueToStore = value instanceof Function ? value(storedValue) : value; 39 | 40 | setStoredValue(valueToStore); 41 | 42 | if (typeof window !== 'undefined') { 43 | window.localStorage.setItem(keyName, JSON.stringify(valueToStore)); 44 | } 45 | } catch (err) { 46 | // TODO: Handle these error cases 47 | // eslint-disable-next-line no-console 48 | console.error(err); 49 | } 50 | }; 51 | 52 | return [storedValue, setValue]; 53 | }; 54 | 55 | export default useLocalStorage; 56 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const CompressionPlugin = require('compression-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const { merge } = require('webpack-merge'); 4 | const S3Plugin = require('webpack-s3-plugin'); 5 | 6 | const commonConfig = require('./webpack.config.common'); 7 | 8 | require('dotenv').config({ path: './.env.production' }); 9 | 10 | const { COMPRESS_ASSETS = null, DEPLOY_TO_S3 = null } = process.env; 11 | 12 | /** 13 | * @typedef {import('webpack').Configuration} Configuration 14 | * @type {Configuration} 15 | * 16 | * @see https://webpack.js.org/configuration/ 17 | */ 18 | const config = { 19 | mode: 'production', 20 | stats: 'detailed', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | process: { 24 | env: { 25 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 26 | APP_API_URL: JSON.stringify(process.env.APP_API_URL), 27 | APP_ASSET_URL: JSON.stringify(process.env.APP_ASSET_URL), 28 | }, 29 | }, 30 | }), 31 | COMPRESS_ASSETS && 32 | new CompressionPlugin({ 33 | test: /\.(js|css)$/, 34 | filename: '[path][base]', 35 | algorithm: 'gzip', 36 | deleteOriginalAssets: true, 37 | }), 38 | DEPLOY_TO_S3 && 39 | new S3Plugin({ 40 | s3Options: { 41 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 42 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 43 | }, 44 | s3UploadOptions: { 45 | ACL: '', 46 | Bucket: process.env.FRONTEND_BUCKET_NAME, // Your bucket name 47 | // eslint-disable-next-line consistent-return 48 | ContentEncoding(fileName) { 49 | if (/\.(js|css)$/.test(fileName)) { 50 | return 'gzip'; 51 | } 52 | }, 53 | // eslint-disable-next-line consistent-return 54 | ContentType(fileName) { 55 | if (/\.css/.test(fileName)) { 56 | return 'text/css'; 57 | } 58 | 59 | if (/\.js/.test(fileName)) { 60 | return 'application/javascript'; 61 | } 62 | }, 63 | }, 64 | cloudfrontInvalidateOptions: { 65 | DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID, 66 | Items: ['/*'], 67 | }, 68 | directory: 'dist', // This is the directory you want to upload 69 | }), 70 | ].filter(Boolean), 71 | }; 72 | 73 | module.exports = merge(commonConfig, config); 74 | -------------------------------------------------------------------------------- /src/assets/fonts/stylesheet.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'inter_thin'; 3 | font-style: normal; 4 | font-weight: 100; 5 | font-display: swap; 6 | src: 7 | url('inter-thin.woff2?v=3.19') format('woff2'), 8 | url('inter-thin.woff?v=3.19') format('woff'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'inter_extralight'; 13 | font-style: normal; 14 | font-weight: 200; 15 | font-display: swap; 16 | src: 17 | url('inter-extralight.woff2?v=3.19') format('woff2'), 18 | url('inter-extralight.woff?v=3.19') format('woff'); 19 | } 20 | 21 | @font-face { 22 | font-family: 'inter_light'; 23 | font-style: normal; 24 | font-weight: 300; 25 | font-display: swap; 26 | src: 27 | url('inter-light.woff2?v=3.19') format('woff2'), 28 | url('inter-light.woff?v=3.19') format('woff'); 29 | } 30 | 31 | @font-face { 32 | font-family: 'inter_regular'; 33 | font-style: normal; 34 | font-weight: 400; 35 | font-display: swap; 36 | src: 37 | url('inter-regular.woff2?v=3.19') format('woff2'), 38 | url('inter-regular.woff?v=3.19') format('woff'); 39 | } 40 | 41 | @font-face { 42 | font-family: 'inter_medium'; 43 | font-style: normal; 44 | font-weight: 500; 45 | font-display: swap; 46 | src: 47 | url('inter-medium.woff2?v=3.19') format('woff2'), 48 | url('inter-medium.woff?v=3.19') format('woff'); 49 | } 50 | 51 | @font-face { 52 | font-family: 'inter_semibold'; 53 | font-style: normal; 54 | font-weight: 600; 55 | font-display: swap; 56 | src: 57 | url('inter-semibold.woff2?v=3.19') format('woff2'), 58 | url('inter-semibold.woff?v=3.19') format('woff'); 59 | } 60 | 61 | @font-face { 62 | font-family: 'inter_bold'; 63 | font-style: normal; 64 | font-weight: 700; 65 | font-display: swap; 66 | src: 67 | url('inter-bold.woff2?v=3.19') format('woff2'), 68 | url('inter-bold.woff?v=3.19') format('woff'); 69 | } 70 | 71 | @font-face { 72 | font-family: 'inter_extrabold'; 73 | font-style: normal; 74 | font-weight: 800; 75 | font-display: swap; 76 | src: 77 | url('inter-extrabold.woff2?v=3.19') format('woff2'), 78 | url('inter-extrabold.woff?v=3.19') format('woff'); 79 | } 80 | 81 | @font-face { 82 | font-family: 'inter_black'; 83 | font-style: normal; 84 | font-weight: 900; 85 | font-display: swap; 86 | src: 87 | url('inter-black.woff2?v=3.19') format('woff2'), 88 | url('inter-black.woff?v=3.19') format('woff'); 89 | } 90 | 91 | /* ------------------------------------------------------- 92 | Variable font. 93 | Usage: 94 | 95 | html { font-family: 'inter', sans-serif; } 96 | @supports (font-variation-settings: normal) { 97 | html { font-family: 'inter var', sans-serif; } 98 | } 99 | */ 100 | @font-face { 101 | font-family: 'inter_var'; 102 | font-weight: 100 900; 103 | font-display: swap; 104 | font-style: normal; 105 | src: url('inter-roman.var.woff2?v=3.19') format('woff2'); 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | React Starter 10 | 11 |
12 |

13 | 14 | ## Development Setup 15 | 16 | ### Use Tailwind 17 | 18 | Simply add import for [tailwind.scss](src/styles/tailwind.scss) in [index.js](src/index.js) 19 | 20 | ### Using SVG 21 | 22 | There are two ways to import SVG, as an inline SVG and as a Image. Depending on the use case, 23 | for loading SVG as a Image append `?url` while importing, this will not inline the SVG, whereas 24 | default import will inline the SVG content. 25 | 26 | ```js 27 | import Svg from './assets/file.svg?react'; //<--- SVG as React component 28 | import svg from './assets/file.svg'; // <--- SVG as Image 29 | 30 | const App = () => { 31 | return ( 32 |
33 | 34 | 35 |
36 | ); 37 | }; 38 | ``` 39 | 40 | ## AWS Build 41 | 42 | The project comes with default settings for hosting on AWS S3 using Cloudfront. The setup is 43 | optimized to reduce the client payload size by compressing JS, CSS assets in gzip format. To 44 | achieve this, it uses the `compression-webpack-plugin` to compress the assets before uploading 45 | them to S3 using the `webpack-s3-plugin`. 46 | 47 | For taking advantage of this, below AWS configuration are required 48 | 49 | ```env 50 | DEPLOY_TO_S3=Y 51 | 52 | AWS_ACCESS_KEY_ID= 53 | AWS_SECRET_ACCESS_KEY= 54 | FRONTEND_BUCKET_NAME= 55 | ``` 56 | 57 | ### Git 58 | 59 | There are two branches 60 | 61 | - `develop` for active development 62 | - `main` official branch used for production releases 63 | 64 | First clone this repository 65 | 66 | ```text 67 | git clone git@github.com:akash-gajjar/react-starter.git 68 | ``` 69 | 70 | or using ssh 71 | 72 | ```text 73 | git clone git@github.com:akash-gajjar/react-starter.git 74 | ``` 75 | 76 | Checkout `develop` or `main` and then install dependencies 77 | 78 | ```sh 79 | git checkout develop 80 | yarn install 81 | ``` 82 | 83 | Finally start development server 84 | 85 | ```sh 86 | yarn start 87 | ``` 88 | 89 | ## Contributing 90 | 91 | Checkout new branch from your local develop branch, add meaningful branch name with prefix like 92 | `fix/` when fixing a bug or `feat/` when adding a new feature. 93 | 94 | ```text 95 | git checkout -b fix/meaningful-name 96 | ``` 97 | 98 | After making changes to your local git repository make sure to run linter 99 | scripts 100 | 101 | ```text 102 | yarn lint:style 103 | yarn lint:script 104 | ``` 105 | 106 | If you have any linting error Fix them now by running 107 | 108 | ```text 109 | yarn lint:fix 110 | ``` 111 | 112 | and then finally format your code using `prettier` 113 | 114 | ```text 115 | yarn prettier 116 | ``` 117 | 118 | Now commit your changes and provide title and summary of changes then execute 119 | 120 | ```text 121 | git push 122 | ``` 123 | 124 | Now you can submit a PR. 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,yarn,react 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn,react 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Optional stylelint cache 63 | .stylelintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variable files 81 | .env 82 | .env.development.local 83 | .env.development 84 | .env.test.local 85 | .env.production.local 86 | .env.production 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | ### Node Patch ### 139 | # Serverless Webpack directories 140 | .webpack/ 141 | 142 | # Optional stylelint cache 143 | 144 | # SvelteKit build / generate output 145 | .svelte-kit 146 | 147 | ### react ### 148 | .DS_* 149 | **/*.backup.* 150 | **/*.back.* 151 | 152 | node_modules 153 | 154 | *.sublime* 155 | 156 | psd 157 | thumb 158 | sketch 159 | 160 | ### yarn ### 161 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 162 | 163 | .yarn/* 164 | !.yarn/releases 165 | !.yarn/patches 166 | !.yarn/plugins 167 | !.yarn/sdks 168 | !.yarn/versions 169 | 170 | # if you are NOT using Zero-installs, then: 171 | # comment the following lines 172 | !.yarn/cache 173 | 174 | # and uncomment the following lines 175 | # .pnp.* 176 | 177 | # End of https://www.toptal.com/developers/gitignore/api/node,yarn,react 178 | 179 | *.zip 180 | 181 | megalinter-reports/ 182 | -------------------------------------------------------------------------------- /webpack.config.common.js: -------------------------------------------------------------------------------- 1 | const childprocess = require('child_process'); 2 | const path = require('path'); 3 | 4 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | const CopyPlugin = require('copy-webpack-plugin'); 7 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 10 | const postcssNormalize = require('postcss-normalize'); 11 | const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); 12 | 13 | const gitRevision = 0; //childprocess.execSync('git rev-parse HEAD').toString().trim(); 14 | 15 | module.exports = { 16 | context: path.resolve(__dirname), 17 | target: 'web', 18 | entry: path.join(__dirname, 'src', 'index.js'), 19 | output: { 20 | filename: '[name].[contenthash].bundle.js', 21 | path: path.resolve(__dirname, './dist'), 22 | publicPath: '/', 23 | }, 24 | optimization: { 25 | minimizer: [new CssMinimizerPlugin()], 26 | }, 27 | plugins: [ 28 | new CaseSensitivePathsPlugin(), 29 | new MiniCssExtractPlugin({ 30 | filename: process.env.NODE_ENV === 'production' ? '[name]-[contenthash].css' : '[name].css', 31 | }), 32 | new CleanWebpackPlugin(), 33 | new HtmlWebpackPlugin({ 34 | template: './public/index.html', 35 | filename: './index.html', 36 | templateParameters: { 37 | gitRevision, 38 | buildTime: new Date().toISOString(), 39 | }, 40 | }), 41 | new CopyPlugin({ 42 | patterns: [{ from: './public/images', to: 'images', noErrorOnMissing: true }], 43 | }), 44 | new WebpackManifestPlugin(), 45 | ], 46 | resolve: { 47 | modules: ['node_modules'], 48 | alias: { 49 | public: path.resolve(__dirname, './public/'), 50 | extensions: ['.js', '.jsx', '.css', '.scss', '.json'], 51 | }, 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.(js|jsx)$/, 57 | exclude: path.resolve(__dirname, 'node_modules'), 58 | use: ['babel-loader'], 59 | }, 60 | { 61 | test: /\.(png|jpg|gif|jpeg|webp|ico)$/, 62 | use: [ 63 | { 64 | loader: 'url-loader', 65 | options: { 66 | limit: 8192, // in bytes 67 | fallback: require.resolve('file-loader'), 68 | name: process.env.NODE_ENV === 'production' ? '[name]-[contenthash].[ext]' : '[name].[ext]', 69 | }, 70 | }, 71 | ], 72 | }, 73 | { 74 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 75 | type: 'asset/resource', 76 | generator: { 77 | filename: './fonts/[name][ext]', 78 | }, 79 | }, 80 | { 81 | test: /\.svg$/, 82 | oneOf: [ 83 | { 84 | issuer: /\.[jt]sx?$/, 85 | resourceQuery: /react/, // *.svg?react 86 | use: ['@svgr/webpack'], 87 | }, 88 | { 89 | type: 'asset', 90 | parser: { 91 | dataUrlCondition: { 92 | maxSize: 200, 93 | }, 94 | }, 95 | }, 96 | ], 97 | }, 98 | { 99 | test: /\.less$/, 100 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 101 | }, 102 | { 103 | test: /\.s?css$/, 104 | use: [ 105 | { 106 | loader: MiniCssExtractPlugin.loader, 107 | options: {}, 108 | }, 109 | 'css-loader', 110 | { 111 | loader: 'sass-loader', 112 | options: { 113 | sourceMap: process.env.NODE_ENV !== 'production', // <-- !!IMPORTANT!! 114 | }, 115 | }, 116 | { 117 | loader: 'postcss-loader', 118 | options: { 119 | postcssOptions: { 120 | ident: 'postcss', 121 | plugins: () => [postcssNormalize()], 122 | }, 123 | }, 124 | }, 125 | ], 126 | }, 127 | ], 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-starter", 3 | "description": "React frontend starter template.", 4 | "version": "0.0.0", 5 | "private": true, 6 | "author": { 7 | "name": "Aakash Gajjar", 8 | "email": "hello@aakashgajjar.dev", 9 | "url": "https://aakashgajjar.dev" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://github.com/akash-gajjar/react-starter#readme", 13 | "repository": "git@github.com:akash-gajjar/react-starter.git", 14 | "main": "./src/index.js", 15 | "scripts": { 16 | "build:dev": "NODE_ENV=production webpack --env env=production --mode production --config=webpack.config.prod.js", 17 | "build:prod": "NODE_ENV=production webpack --env env=production --mode production --config=webpack.config.prod.js", 18 | "lint:fix": "eslint --fix . --ext .js,.jsx", 19 | "lint:script": "eslint . --ext .js,.jsx", 20 | "lint:style": "stylelint './src/**/*.js'", 21 | "prettier": "prettier -c -w .", 22 | "start": "NODE_OPTIONS=--max_old_space_size=2048 NODE_ENV=development webpack-dev-server --env env=development --mode development --config=webpack.config.dev.js --open --progress", 23 | "serve": "serve -p 6661 -s dist/" 24 | }, 25 | "dependencies": { 26 | "@tanstack/react-query": "^5.28.8", 27 | "@tanstack/react-query-devtools": "^5.28.8", 28 | "axios": "^1.6.8", 29 | "date-fns": "^3.6.0", 30 | "history": "^5.3.0", 31 | "jwt-decode": "^4.0.0", 32 | "lodash": "^4.17.21", 33 | "normalize.css": "^8.0.1", 34 | "prop-types": "^15.8.1", 35 | "qs": "^6.12.0", 36 | "react": "^18.2.0", 37 | "react-content-loader": "^7.0.0", 38 | "react-dom": "^18.2.0", 39 | "react-error-boundary": "^4.0.13", 40 | "react-hook-form": "^7.51.1", 41 | "react-icons": "^5.0.1", 42 | "react-responsive": "^10.0.0", 43 | "react-router-dom": "^6.22.3", 44 | "styled-components": "^6.1.8", 45 | "uuid": "^9.0.1" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.24.3", 49 | "@babel/plugin-proposal-class-properties": "^7.18.6", 50 | "@babel/plugin-proposal-optional-chaining": "^7.21.0", 51 | "@babel/preset-env": "^7.24.3", 52 | "@babel/preset-react": "^7.24.1", 53 | "@babel/types": "^7.24.0", 54 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", 55 | "@sigmacomputing/babel-plugin-lodash": "^3.3.5", 56 | "@svgr/webpack": "^8.1.0", 57 | "autoprefixer": "^10.4.19", 58 | "babel-eslint": "^10.1.0", 59 | "babel-loader": "^9.1.3", 60 | "babel-plugin-module-resolver": "^5.0.0", 61 | "babel-plugin-styled-components": "^2.1.4", 62 | "case-sensitive-paths-webpack-plugin": "^2.4.0", 63 | "clean-webpack-plugin": "^4.0.0", 64 | "compression-webpack-plugin": "^11.1.0", 65 | "copy-webpack-plugin": "^12.0.2", 66 | "css-loader": "^6.10.0", 67 | "css-minimizer-webpack-plugin": "^6.0.0", 68 | "cssnano": "^6.1.2", 69 | "dotenv": "^16.4.5", 70 | "eslint": "^8.57.0", 71 | "eslint-config-airbnb": "^19.0.4", 72 | "eslint-config-prettier": "^9.1.0", 73 | "eslint-import-resolver-babel-module": "^5.3.2", 74 | "eslint-plugin-import": "^2.29.1", 75 | "eslint-plugin-jsx-a11y": "^6.8.0", 76 | "eslint-plugin-module-resolver": "^1.5.0", 77 | "eslint-plugin-prettier": "^5.1.3", 78 | "eslint-plugin-react": "^7.34.1", 79 | "eslint-plugin-react-hooks": "^4.6.0", 80 | "eslint-plugin-unused-imports": "^3.1.0", 81 | "file-loader": "^6.2.0", 82 | "html-webpack-plugin": "^5.6.0", 83 | "identity-obj-proxy": "^3.0.0", 84 | "jest": "^29.7.0", 85 | "mini-css-extract-plugin": "^2.8.1", 86 | "postcss": "^8.4.38", 87 | "postcss-loader": "^8.1.1", 88 | "postcss-normalize": "^10.0.1", 89 | "postcss-preset-env": "^9.5.2", 90 | "prettier": "^3.2.5", 91 | "react-refresh": "^0.14.0", 92 | "regenerator-runtime": "^0.14.1", 93 | "sass": "^1.72.0", 94 | "sass-loader": "^14.1.1", 95 | "serve": "^14.2.1", 96 | "style-loader": "^3.3.4", 97 | "stylelint": "^16.3.0", 98 | "stylelint-config-recommended": "^14.0.0", 99 | "stylelint-config-styled-components": "^0.1.1", 100 | "stylelint-processor-styled-components": "^1.10.0", 101 | "stylelint-webpack-plugin": "^5.0.0", 102 | "tailwindcss": "^3.4.1", 103 | "terser-webpack-plugin": "^5.3.10", 104 | "url-loader": "^4.1.1", 105 | "webpack": "^5.91.0", 106 | "webpack-bundle-analyzer": "^4.10.1", 107 | "webpack-cli": "^5.1.4", 108 | "webpack-dev-server": "^5.0.4", 109 | "webpack-manifest-plugin": "^5.0.0", 110 | "webpack-merge": "^5.10.0", 111 | "webpack-s3-plugin": "1.2.0-rc.0" 112 | }, 113 | "engines": { 114 | "node": ">=18.0.0" 115 | }, 116 | "browserslist": { 117 | "production": [ 118 | "defaults and supports es6-module", 119 | "maintained node versions" 120 | ], 121 | "development": [ 122 | "last 1 chrome version", 123 | "last 1 firefox version", 124 | "last 1 safari version" 125 | ] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("eslint").Linter.Config} 3 | */ 4 | const config = { 5 | root: true, 6 | env: { 7 | browser: true, 8 | es2021: true, 9 | jest: true, 10 | }, 11 | extends: [ 12 | 'airbnb', 13 | 'eslint:recommended', 14 | 'plugin:import/recommended', 15 | 'plugin:react/recommended', 16 | 'plugin:prettier/recommended', 17 | 'plugin:react/jsx-runtime', 18 | ], 19 | overrides: [], 20 | parserOptions: { 21 | ecmaVersion: 'latest', 22 | sourceType: 'module', 23 | ecmaFeatures: { 24 | jsx: true, 25 | }, 26 | }, 27 | plugins: ['import', 'unused-imports', 'react', 'react-hooks', 'prettier'], 28 | settings: { 29 | 'import/extensions': ['.js', '.jsx', '.json', '.css', '.scss', '.svg', '.png'], 30 | 'import/resolver': { 31 | 'babel-module': {}, 32 | }, 33 | }, 34 | // react See: https://github.com/jsx-eslint/eslint-plugin-react 35 | // react-hooks See: https://reactjs.org/docs/hooks-rules.html 36 | rules: { 37 | 'prettier/prettier': [ 38 | 'error', 39 | { 40 | semi: true, 41 | singleQuote: true, 42 | trailingComma: 'es5', 43 | }, 44 | ], 45 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 46 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 47 | 'react/hook-use-state': 'warn', 48 | 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 49 | 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies 50 | 'react/jsx-uses-react': 'warn', 51 | 'react/jsx-uses-vars': 'warn', 52 | 'react/no-array-index-key': 'warn', // FIXME: enable this when integration 53 | 'react/function-component-definition': [ 54 | 'warn', 55 | { 56 | namedComponents: 'arrow-function', 57 | unnamedComponents: 'arrow-function', 58 | }, 59 | ], 60 | 'react/no-unstable-nested-components': [ 61 | 'warn', 62 | { 63 | allowAsProps: true, 64 | }, 65 | ], 66 | 'jsx-a11y/anchor-is-valid': 'warn', 67 | 'react/jsx-filename-extension': [ 68 | 1, 69 | { 70 | extensions: ['.js', '.jsx'], 71 | }, 72 | ], 73 | 'react/jsx-props-no-spreading': 'off', 74 | 'react/prop-types': 'error', 75 | 'import/first': 'error', 76 | 'import/newline-after-import': 'error', 77 | 'import/no-duplicates': 'error', 78 | 'import/no-extraneous-dependencies': 'error', 79 | 'import/no-unassigned-import': [ 80 | 'error', 81 | { 82 | allow: ['**/*.css', '**/*.scss'], 83 | }, 84 | ], 85 | 'import/order': [ 86 | 'error', 87 | { 88 | alphabetize: { 89 | order: 'asc', 90 | }, 91 | groups: ['builtin', 'external', 'parent', 'sibling', 'index'], 92 | 'newlines-between': 'always', 93 | pathGroups: [ 94 | { 95 | group: 'builtin', 96 | pattern: 'react', 97 | position: 'before', 98 | }, 99 | { 100 | group: 'internal', 101 | pattern: '#/**', 102 | position: 'after', 103 | }, 104 | ], 105 | pathGroupsExcludedImportTypes: ['builtin'], 106 | }, 107 | ], 108 | 'import/prefer-default-export': 'off', 109 | 'import/no-unresolved': [ 110 | 'error', 111 | { 112 | caseSensitive: false, 113 | ignore: ['.svg'], 114 | }, 115 | ], 116 | 'import/extensions': 'off', 117 | 'no-unused-vars': 'off', 118 | 'unused-imports/no-unused-imports': 'error', 119 | 'unused-imports/no-unused-vars': [ 120 | 'warn', 121 | { 122 | vars: 'all', 123 | varsIgnorePattern: '^_', 124 | args: 'after-used', 125 | argsIgnorePattern: '^_', 126 | }, 127 | ], 128 | camelcase: [ 129 | 'error', 130 | { 131 | properties: 'always', 132 | }, 133 | ], 134 | 'no-underscore-dangle': [ 135 | 'error', 136 | { 137 | allow: ['_id', '_query'], 138 | }, 139 | ], 140 | indent: [ 141 | 'error', 142 | 2, 143 | { 144 | SwitchCase: 1, 145 | }, 146 | ], 147 | 'max-len': [ 148 | 'warn', 149 | { 150 | code: 120, 151 | comments: 80, 152 | ignoreComments: false, 153 | ignoreRegExpLiterals: true, 154 | ignoreStrings: true, 155 | ignoreTemplateLiterals: true, 156 | ignoreTrailingComments: true, 157 | ignoreUrls: true, 158 | tabWidth: 2, 159 | }, 160 | ], 161 | 'newline-before-return': 'error', 162 | 'no-duplicate-imports': 'error', 163 | 'no-multi-spaces': ['error'], 164 | 'no-multiple-empty-lines': [ 165 | 'error', 166 | { 167 | max: 1, 168 | maxBOF: 0, 169 | maxEOF: 0, 170 | }, 171 | ], 172 | 'padding-line-between-statements': [ 173 | 'error', 174 | { 175 | blankLine: 'always', 176 | next: 'cjs-export', 177 | prev: '*', 178 | }, 179 | { 180 | blankLine: 'always', 181 | next: 'export', 182 | prev: '*', 183 | }, 184 | { 185 | blankLine: 'always', 186 | next: 'return', 187 | prev: '*', 188 | }, 189 | { 190 | blankLine: 'always', 191 | next: 'if', 192 | prev: '*', 193 | }, 194 | ], 195 | quotes: [ 196 | 'error', 197 | 'single', 198 | { 199 | allowTemplateLiterals: true, 200 | }, 201 | ], 202 | semi: ['error', 'always'], 203 | 'sort-imports': [ 204 | 'error', 205 | { 206 | allowSeparatedGroups: true, 207 | ignoreDeclarationSort: true, 208 | }, 209 | ], 210 | 'default-param-last': 'warn', 211 | }, 212 | }; 213 | 214 | module.exports = config; 215 | --------------------------------------------------------------------------------