├── .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 | resetErrorBoundary()}>
14 | Try Again
15 |
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 | You need to enable JavaScript to run this app.
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 |
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 |
--------------------------------------------------------------------------------