diff --git a/.github/workflows/merge-jobs.yml b/.github/workflows/merge-jobs.yml index ff1beea..a413ca2 100644 --- a/.github/workflows/merge-jobs.yml +++ b/.github/workflows/merge-jobs.yml @@ -48,6 +48,17 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Setup Next.js cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + - name: Run merge tasks run: | pnpm install diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..b4b0bdd --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,96 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy to GitHub Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["master"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Cancel Workflow Action + uses: styfle/cancel-workflow-action@0.12.1 + with: + access_token: ${{ github.token }} + + - name: Checkout + uses: actions/checkout@v4 + + - name: Read .nvmrc + run: echo ::set-output name=NVMRC::$(cat .nvmrc) + id: nvm + + - name: Use Node.js ${{ steps.nvm.outputs.NVMRC }} + uses: actions/setup-node@v4 + with: + node-version: ${{ steps.nvm.outputs.NVMRC }} + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Setup Next.js cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + + - name: Build docs + run: | + pnpm install + pnpm run build + + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + # Automatically inject basePath in your Next.js configuration file and disable + # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). + static_site_generator: next + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './build' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pull-request-jobs.yml b/.github/workflows/pull-request-jobs.yml index 52c918b..fe393c8 100644 --- a/.github/workflows/pull-request-jobs.yml +++ b/.github/workflows/pull-request-jobs.yml @@ -48,6 +48,17 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Setup Next.js cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + - name: Run test tasks run: | pnpm install @@ -56,3 +67,6 @@ jobs: pnpm run lint:tsc pnpm run build pnpm run test --silent + pnpm run generate:component Foo --dry-run + pnpm run generate:component-loading Foo --dry-run + pnpm run generate:feature Foo --dry-run diff --git a/app/icon.ico b/app/icon.ico new file mode 100644 index 0000000..354202a Binary files /dev/null and b/app/icon.ico differ diff --git a/src/pages/index.css b/app/index.css similarity index 64% rename from src/pages/index.css rename to app/index.css index 4471d80..43ac7ad 100644 --- a/src/pages/index.css +++ b/app/index.css @@ -2,3 +2,7 @@ body { font-family: system-ui; margin: 0; } + +main { + padding: 36px; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..65a5c92 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,18 @@ +import type {ReactNode} from 'react'; + +import {StoreProvider} from '@/src/state/StoreProvider'; +import './index.css'; + +type Props = { + readonly children: ReactNode; +}; + +export default function RootLayout({children}: Props) { + return ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..2f5dce8 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,22 @@ +import type {Metadata} from 'next'; +import {Fragment} from 'react'; + +import Counter from '@/src/components/Counter'; +import Random from '@/src/components/Random'; +import {NavHeader} from '@/src/layout/NavHeader'; + +export default function IndexPage() { + return ( + + +
+ + +
+
+ ); +} + +export const metadata: Metadata = { + title: 'First page', +}; diff --git a/app/second/page.tsx b/app/second/page.tsx new file mode 100644 index 0000000..8c94242 --- /dev/null +++ b/app/second/page.tsx @@ -0,0 +1,19 @@ +import type {Metadata} from 'next'; +import {Fragment} from 'react'; + +import {NavHeader} from '@/src/layout/NavHeader'; + +export default function IndexPage() { + return ( + + +
+

Boring second page. Use to test global state and routes behavior.

+
+
+ ); +} + +export const metadata: Metadata = { + title: 'First page', +}; diff --git a/generate-react-cli.json b/generate-react-cli.json new file mode 100644 index 0000000..7994208 --- /dev/null +++ b/generate-react-cli.json @@ -0,0 +1,65 @@ +{ + "usesTypeScript": true, + "usesCssModule": true, + "cssPreprocessor": "css", + "testLibrary": "Testing Library", + "usesStyledComponents": false, + "component": { + "default": { + "path": "src/components", + "withLazy": false, + "withStory": false, + "withStyle": true, + "withTest": true, + "withIndex": true, + "withMdx": false, + "withHook": false, + "withHookTest": false, + "customTemplates": { + "component": "templates/Component/TemplateName.tsx", + "style": "templates/Component/TemplateName.module.css", + "index": "templates/Component/index.ts", + "test": "templates/Component/TemplateName.spec.tsx" + } + }, + "loading": { + "path": "src/components", + "withLazy": false, + "withStory": false, + "withStyle": true, + "withTest": true, + "withIndex": true, + "withMdx": false, + "withHook": false, + "withHookTest": false, + "customTemplates": { + "component": "templates/Loading/TemplateName.tsx", + "style": "templates/Loading/TemplateName.module.css", + "index": "templates/Loading/index.ts", + "test": "templates/Loading/TemplateName.spec.tsx" + } + }, + "feature": { + "path": "src/features", + "withActionTypes": true, + "withIndex": true, + "withReadme": true, + "withSelectorsTest": true, + "withSelectors": true, + "withReducerTest": true, + "withQueryTest": true, + "withquery": true, + "customTemplates": { + "component": "templates/Feature/TemplateNameReducer.ts", + "actionTypes": "templates/Feature/actionTypes.ts", + "index": "templates/Feature/index.ts", + "readme": "templates/Feature/README.md", + "selectorsTest": "templates/Feature/selectors.spec.tsx", + "selectors": "templates/Feature/selectors.ts", + "reducerTest": "templates/Feature/TemplateNameReducer.spec.tsx", + "queryTest": "templates/Feature/useGetTemplateNameQuery.spec.tsx", + "query": "templates/Feature/useGetTemplateNameQuery.ts" + } + } + } +} diff --git a/jest.config.cjs b/jest.config.cjs index b6f2d5f..7b99e6b 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -4,7 +4,7 @@ const nextJest = require('next/jest.js'); const createJestConfig = nextJest({ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment dir: './', -}) +}); /** * For a detailed explanation regarding each configuration property, visit: @@ -19,4 +19,5 @@ module.exports = createJestConfig({ '\\.css$': 'identity-obj-proxy', }, setupFilesAfterEnv: ['/src/setupTests.ts'], + modulePathIgnorePatterns: ['/templates/'], }); diff --git a/lint-staged.config.js b/lint-staged.config.js index f58e395..1508f16 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,4 +1,4 @@ module.exports = { '*.{js,jsx,ts,tsx}': ['eslint --fix'], - '*.style.{ts,tsx}': ['stylelint --fix'] + '*.style.{ts,tsx}': ['stylelint --fix'], }; diff --git a/next.config.js b/next.config.js index c0c09bb..4148f84 100644 --- a/next.config.js +++ b/next.config.js @@ -3,9 +3,7 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, distDir: 'build', - compiler: { - styledComponents: true, - }, + output: 'export', }; /* Enable bundle analysis. Run `yarn analyze:build` to get report */ diff --git a/package.json b/package.json index 87683f7..930d35b 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,14 @@ "fix:style": "stylelint 'src/**/*.css' --fix", "lint:tsc": "tsc --pretty --noEmit", "prepare": "is-ci || husky install", - "test": "jest" + "test": "jest", + "generate:component": "npx generate-react-cli component", + "generate:component-loading": "npx generate-react-cli component --type=loading", + "generate:feature": "npx generate-react-cli component --type=feature" }, "dependencies": { - "axios": "1.6.8", - "classnames": "^2.5.1", + "@reduxjs/toolkit": "2.2.4", + "classnames": "2.5.1", "next": "14.2.3", "react": "18.3.1", "react-dom": "18.3.1", @@ -47,6 +50,7 @@ "eslint-plugin-react": "7.34.1", "eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-ssr-friendly": "1.3.0", + "generate-react-cli": "8.4.1", "husky": "8.0.3", "identity-obj-proxy": "3.0.0", "is-ci": "3.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a82b847..73c9a41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,11 +8,11 @@ importers: .: dependencies: - axios: - specifier: 1.6.8 - version: 1.6.8 + '@reduxjs/toolkit': + specifier: 2.2.4 + version: 2.2.4(react-redux@8.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1))(react@18.3.1) classnames: - specifier: ^2.5.1 + specifier: 2.5.1 version: 2.5.1 next: specifier: 14.2.3 @@ -96,6 +96,9 @@ importers: eslint-plugin-ssr-friendly: specifier: 1.3.0 version: 1.3.0(eslint@8.57.0) + generate-react-cli: + specifier: 8.4.1 + version: 8.4.1 husky: specifier: 8.0.3 version: 8.0.3 @@ -1385,6 +1388,10 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@ljharb/through@2.3.13': + resolution: {integrity: sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==} + engines: {node: '>= 0.4'} + '@next/bundle-analyzer@14.2.3': resolution: {integrity: sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA==} @@ -1474,6 +1481,17 @@ packages: '@polka/url@1.0.0-next.25': resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + '@reduxjs/toolkit@2.2.4': + resolution: {integrity: sha512-EoIC9iC2V/DLRBVMXRHrO/oM3QBT7RuJNeBRx8Cpnz/NHINeZBEqgI8YOxAYUjLp+KYxGgc4Wd6KoAKsaUBGhg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rushstack/eslint-patch@1.2.0': resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} @@ -1922,9 +1940,6 @@ packages: resolution: {integrity: sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==} engines: {node: '>=4'} - axios@1.6.8: - resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} - axobject-query@3.1.1: resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} @@ -1989,10 +2004,16 @@ packages: balanced-match@2.0.0: resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base@0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2023,6 +2044,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2069,6 +2093,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -2098,6 +2126,10 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -2110,9 +2142,16 @@ packages: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2121,6 +2160,10 @@ packages: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -2159,6 +2202,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@12.0.0: + resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} + engines: {node: '>=18'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -2351,10 +2398,17 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deep-keys@0.5.0: + resolution: {integrity: sha512-/80a4+9lbLj1hRxG0ULtEOGtbM4hN/5u1Vu6kc6ZkYePUq+ZhtboRIsWTVKplc2ET1xY2FMVwhyt46w9vPf9Rg==} + engines: {node: '>=0.10.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} engines: {node: '>= 0.4'} @@ -2426,6 +2480,10 @@ packages: engines: {node: '>=12'} deprecated: Use your platform's native DOMException instead + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -2811,15 +2869,6 @@ packages: resolution: {integrity: sha512-0OEk9Gr+Yj7wjDW2KgaNYUypKau71jAfFyeLQF5iVtxqc6uJHag/MT7pmaEApf4qM7u86DkBcd4ualddYMfbLw==} engines: {node: '>=0.4.0'} - follow-redirects@1.15.6: - resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -2842,6 +2891,10 @@ packages: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} + fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2860,6 +2913,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generate-react-cli@8.4.1: + resolution: {integrity: sha512-DkhIJSl3EQGT+l6gEpk0499CbwKcb7fRciBK7JWJGHzEI7647Ks2QyAUVMSgvqMshyB1R8HcE421w92k4W5IzA==} + engines: {node: '>=10.x', npm: '>= 6.x'} + hasBin: true + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3097,10 +3155,16 @@ packages: resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} engines: {node: '>=4'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -3143,6 +3207,10 @@ packages: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} + inquirer@9.2.15: + resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} + engines: {node: '>=18'} + internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -3254,6 +3322,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -3334,6 +3406,10 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} @@ -3599,6 +3675,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsx-ast-utils@3.3.3: resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} engines: {node: '>=4.0'} @@ -3691,6 +3770,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + log-update@4.0.0: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} engines: {node: '>=10'} @@ -3793,6 +3876,9 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@3.0.5: + resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3834,6 +3920,10 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4014,6 +4104,10 @@ packages: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -4361,9 +4455,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -4435,6 +4526,10 @@ packages: resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} engines: {node: '>=12'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + recast@0.20.5: resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==} engines: {node: '>= 4'} @@ -4450,6 +4545,11 @@ packages: redux-mock-store@1.5.4: resolution: {integrity: sha512-xmcA0O/tjCLXhh9Fuiq6pMrJCwFRaouA8436zcikdIpYWWCjU76CRk+i2bHx8EeiSiMGnB85/lZdU3wIJVXHTA==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -4501,6 +4601,11 @@ packages: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} + replace@1.2.2: + resolution: {integrity: sha512-C4EDifm22XZM2b2JOYe6Mhn+lBsLBAvLbK8drfUQLTfD1KYl/n3VaW/CDju0Ny4w3xTtegBpg8YNSpFJPUDSjA==} + engines: {node: '>= 6'} + hasBin: true + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4509,9 +4614,15 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -4570,6 +4681,10 @@ packages: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} + run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4580,6 +4695,9 @@ packages: rxjs@7.8.0: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + safe-array-concat@1.0.1: resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} @@ -4588,6 +4706,9 @@ packages: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} @@ -4626,6 +4747,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} engines: {node: '>= 0.4'} @@ -4831,6 +4955,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5152,6 +5279,10 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unset-value@1.0.0: resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} engines: {node: '>=0.10.0'} @@ -5210,6 +5341,9 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -5247,6 +5381,9 @@ packages: which-collection@1.0.1: resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.13: resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} engines: {node: '>= 0.4'} @@ -5321,6 +5458,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5339,6 +5479,10 @@ packages: resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==} engines: {node: '>= 14'} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -5347,6 +5491,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -7375,7 +7523,7 @@ snapshots: collect-v8-coverage: 1.0.2 exit: 0.1.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 istanbul-lib-instrument: 6.0.2 istanbul-lib-report: 3.0.1 @@ -7399,7 +7547,7 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.25 callsites: 3.1.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 '@jest/test-result@29.7.0': dependencies: @@ -7411,7 +7559,7 @@ snapshots: '@jest/test-sequencer@29.7.0': dependencies: '@jest/test-result': 29.7.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 29.7.0 slash: 3.0.0 @@ -7424,7 +7572,7 @@ snapshots: chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 @@ -7480,6 +7628,10 @@ snapshots: '@jridgewell/sourcemap-codec': 1.4.15 optional: true + '@ljharb/through@2.3.13': + dependencies: + call-bind: 1.0.7 + '@next/bundle-analyzer@14.2.3': dependencies: webpack-bundle-analyzer: 4.10.1 @@ -7550,6 +7702,16 @@ snapshots: '@polka/url@1.0.0-next.25': {} + '@reduxjs/toolkit@2.2.4(react-redux@8.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.0 + optionalDependencies: + react: 18.3.1 + react-redux: 8.1.3(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@5.0.1) + '@rushstack/eslint-patch@1.2.0': {} '@rushstack/eslint-patch@1.5.1': {} @@ -7989,7 +8151,7 @@ snapshots: array-buffer-byte-length@1.0.0: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 is-array-buffer: 3.0.2 array-buffer-byte-length@1.0.1: @@ -8058,7 +8220,7 @@ snapshots: arraybuffer.prototype.slice@1.0.2: dependencies: array-buffer-byte-length: 1.0.0 - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.3 get-intrinsic: 1.2.2 @@ -8110,14 +8272,6 @@ snapshots: axe-core@4.6.3: {} - axios@1.6.8: - dependencies: - follow-redirects: 1.15.6 - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axobject-query@3.1.1: dependencies: deep-equal: 2.2.0 @@ -8134,7 +8288,7 @@ snapshots: babel-plugin-istanbul: 6.1.1 babel-preset-jest: 29.6.3(@babel/core@7.24.5) chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color @@ -8260,6 +8414,8 @@ snapshots: balanced-match@2.0.0: {} + base64-js@1.5.1: {} + base@0.11.2: dependencies: cache-base: 1.0.1 @@ -8270,6 +8426,12 @@ snapshots: mixin-deep: 1.3.2 pascalcase: 0.1.1 + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -8318,6 +8480,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -8379,6 +8546,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.3.0: {} + char-regex@1.0.2: {} chardet@0.7.0: {} @@ -8402,6 +8571,8 @@ snapshots: dependencies: restore-cursor: 3.1.0 + cli-spinners@2.9.2: {} + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 @@ -8414,8 +8585,16 @@ snapshots: cli-width@3.0.0: {} + cli-width@4.1.0: {} + client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -8428,6 +8607,8 @@ snapshots: kind-of: 6.0.3 shallow-clone: 3.0.1 + clone@1.0.4: {} + co@4.6.0: {} collect-v8-coverage@1.0.2: {} @@ -8459,6 +8640,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@12.0.0: {} + commander@7.2.0: {} commander@9.5.0: {} @@ -8501,7 +8684,7 @@ snapshots: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-config: 29.7.0(@types/node@18.19.33)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@18.19.33)(typescript@5.4.5)) jest-util: 29.7.0 prompts: 2.4.2 @@ -8628,7 +8811,7 @@ snapshots: deep-equal@2.2.0: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 es-get-iterator: 1.1.3 get-intrinsic: 1.2.2 is-arguments: 1.1.1 @@ -8648,8 +8831,14 @@ snapshots: deep-is@0.1.4: {} + deep-keys@0.5.0: {} + deepmerge@4.3.1: {} + defaults@1.0.4: + dependencies: + clone: 1.0.4 + define-data-property@1.1.1: dependencies: get-intrinsic: 1.2.2 @@ -8714,6 +8903,8 @@ snapshots: dependencies: webidl-conversions: 7.0.0 + dotenv@16.4.5: {} + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -8730,7 +8921,7 @@ snapshots: enhanced-resolve@5.12.0: dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 tapable: 2.2.1 entities@4.5.0: {} @@ -8838,7 +9029,7 @@ snapshots: es-get-iterator@1.1.3: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 has-symbols: 1.0.3 is-arguments: 1.1.1 @@ -9410,8 +9601,6 @@ snapshots: flow-parser@0.236.0: {} - follow-redirects@1.15.6: {} - for-each@0.3.3: dependencies: is-callable: 1.2.7 @@ -9435,6 +9624,12 @@ snapshots: dependencies: map-cache: 0.2.2 + fs-extra@11.2.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -9444,13 +9639,24 @@ snapshots: function.prototype.name@1.1.6: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.3 functions-have-names: 1.2.3 functions-have-names@1.2.3: {} + generate-react-cli@8.4.1: + dependencies: + chalk: 5.3.0 + commander: 12.0.0 + deep-keys: 0.5.0 + dotenv: 16.4.5 + fs-extra: 11.2.0 + inquirer: 9.2.15 + lodash: 4.17.21 + replace: 1.2.2 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -9476,7 +9682,7 @@ snapshots: get-symbol-description@1.0.0: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 get-symbol-description@1.0.2: @@ -9689,8 +9895,12 @@ snapshots: dependencies: harmony-reflect: 1.6.2 + ieee754@1.2.1: {} + ignore@5.2.4: {} + immer@10.1.1: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -9736,6 +9946,24 @@ snapshots: strip-ansi: 6.0.1 through: 2.3.8 + inquirer@9.2.15: + dependencies: + '@ljharb/through': 2.3.13 + ansi-escapes: 4.3.2 + chalk: 5.3.0 + cli-cursor: 3.1.0 + cli-width: 4.1.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + internal-slot@1.0.6: dependencies: get-intrinsic: 1.2.2 @@ -9754,12 +9982,12 @@ snapshots: is-arguments@1.1.1: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 has-tostringtag: 1.0.0 is-array-buffer@3.0.2: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 is-typed-array: 1.1.12 @@ -9780,7 +10008,7 @@ snapshots: is-boolean-object@1.1.2: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 has-tostringtag: 1.0.0 is-buffer@1.1.6: {} @@ -9845,6 +10073,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-interactive@1.0.0: {} + is-map@2.0.2: {} is-negative-zero@2.0.2: {} @@ -9875,14 +10105,14 @@ snapshots: is-regex@1.1.4: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 has-tostringtag: 1.0.0 is-set@2.0.2: {} is-shared-array-buffer@1.0.2: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 is-shared-array-buffer@1.0.3: dependencies: @@ -9908,15 +10138,17 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-unicode-supported@0.1.0: {} + is-weakmap@2.0.1: {} is-weakref@1.0.2: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 is-weakset@2.0.2: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 is-windows@1.0.2: {} @@ -10053,7 +10285,7 @@ snapshots: ci-info: 3.8.0 deepmerge: 4.3.1 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-circus: 29.7.0(babel-plugin-macros@3.1.0) jest-environment-node: 29.7.0 jest-get-type: 29.6.3 @@ -10126,7 +10358,7 @@ snapshots: '@types/node': 18.19.33 anymatch: 3.1.3 fb-watchman: 2.0.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 @@ -10153,7 +10385,7 @@ snapshots: '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 micromatch: 4.0.5 pretty-format: 29.7.0 slash: 3.0.0 @@ -10181,7 +10413,7 @@ snapshots: jest-resolve@29.7.0: dependencies: chalk: 4.1.2 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 29.7.0 jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 @@ -10200,7 +10432,7 @@ snapshots: '@types/node': 18.19.33 chalk: 4.1.2 emittery: 0.13.1 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-docblock: 29.7.0 jest-environment-node: 29.7.0 jest-haste-map: 29.7.0 @@ -10230,7 +10462,7 @@ snapshots: cjs-module-lexer: 1.3.1 collect-v8-coverage: 1.0.2 glob: 7.2.3 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-mock: 29.7.0 @@ -10256,7 +10488,7 @@ snapshots: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.5) chalk: 4.1.2 expect: 29.7.0 - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 jest-diff: 29.7.0 jest-get-type: 29.6.3 jest-matcher-utils: 29.7.0 @@ -10407,6 +10639,12 @@ snapshots: json5@2.2.3: {} + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsx-ast-utils@3.3.3: dependencies: array-includes: 3.1.7 @@ -10479,7 +10717,7 @@ snapshots: load-json-file@4.0.0: dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 parse-json: 4.0.0 pify: 3.0.0 strip-bom: 3.0.0 @@ -10507,6 +10745,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + log-update@4.0.0: dependencies: ansi-escapes: 4.3.2 @@ -10616,6 +10859,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@3.0.5: + dependencies: + brace-expansion: 1.1.11 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -10651,6 +10898,8 @@ snapshots: mute-stream@0.0.8: {} + mute-stream@1.0.0: {} + nanoid@3.3.7: {} nanomatch@1.2.13: @@ -10770,7 +11019,7 @@ snapshots: object-is@1.1.5: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 object-keys@1.1.1: {} @@ -10781,7 +11030,7 @@ snapshots: object.assign@4.1.4: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -10868,6 +11117,18 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + os-tmpdir@1.0.2: {} p-limit@2.3.0: @@ -11241,8 +11502,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - proxy-from-env@1.1.0: {} - psl@1.9.0: {} punycode@2.3.0: {} @@ -11305,6 +11564,12 @@ snapshots: parse-json: 5.2.0 type-fest: 1.4.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + recast@0.20.5: dependencies: ast-types: 0.14.2 @@ -11326,6 +11591,10 @@ snapshots: dependencies: lodash.isplainobject: 4.0.6 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + redux@4.2.1: dependencies: '@babel/runtime': 7.21.5 @@ -11360,7 +11629,7 @@ snapshots: regexp.prototype.flags@1.5.1: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 set-function-name: 2.0.1 @@ -11388,12 +11657,22 @@ snapshots: repeat-string@1.6.1: {} + replace@1.2.2: + dependencies: + chalk: 2.4.2 + minimatch: 3.0.5 + yargs: 15.4.1 + require-directory@2.1.1: {} require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + requires-port@1.0.0: {} + reselect@5.1.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -11441,6 +11720,8 @@ snapshots: run-async@2.4.1: {} + run-async@3.0.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -11453,9 +11734,13 @@ snapshots: dependencies: tslib: 2.6.2 + rxjs@7.8.1: + dependencies: + tslib: 2.6.2 + safe-array-concat@1.0.1: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 has-symbols: 1.0.3 isarray: 2.0.5 @@ -11467,9 +11752,11 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-regex-test@1.0.0: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 is-regex: 1.1.4 @@ -11503,6 +11790,8 @@ snapshots: semver@7.6.2: {} + set-blocking@2.0.0: {} + set-function-length@1.1.1: dependencies: define-data-property: 1.1.1 @@ -11559,7 +11848,7 @@ snapshots: side-channel@1.0.4: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 object-inspect: 1.13.1 @@ -11731,7 +12020,7 @@ snapshots: string.prototype.trim@1.2.8: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.3 @@ -11744,7 +12033,7 @@ snapshots: string.prototype.trimend@1.0.7: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.3 @@ -11756,7 +12045,7 @@ snapshots: string.prototype.trimstart@1.0.7: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.3 @@ -11766,6 +12055,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -12035,7 +12328,7 @@ snapshots: typed-array-buffer@1.0.0: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 get-intrinsic: 1.2.2 is-typed-array: 1.1.12 @@ -12047,7 +12340,7 @@ snapshots: typed-array-byte-length@1.0.0: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -12063,7 +12356,7 @@ snapshots: typed-array-byte-offset@1.0.0: dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.5 + call-bind: 1.0.7 for-each: 0.3.3 has-proto: 1.0.1 is-typed-array: 1.1.12 @@ -12079,7 +12372,7 @@ snapshots: typed-array-length@1.0.4: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 for-each: 0.3.3 is-typed-array: 1.1.12 @@ -12098,7 +12391,7 @@ snapshots: unbox-primitive@1.0.2: dependencies: - call-bind: 1.0.5 + call-bind: 1.0.7 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 @@ -12127,6 +12420,8 @@ snapshots: universalify@0.2.0: {} + universalify@2.0.1: {} + unset-value@1.0.0: dependencies: has-value: 0.3.1 @@ -12199,6 +12494,10 @@ snapshots: dependencies: makeerror: 1.0.12 + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -12268,10 +12567,12 @@ snapshots: is-weakmap: 2.0.1 is-weakset: 2.0.2 + which-module@2.0.1: {} + which-typed-array@1.1.13: dependencies: available-typed-arrays: 1.0.5 - call-bind: 1.0.5 + call-bind: 1.0.7 for-each: 0.3.3 gopd: 1.0.1 has-tostringtag: 1.0.0 @@ -12314,7 +12615,7 @@ snapshots: write-file-atomic@2.4.3: dependencies: - graceful-fs: 4.2.10 + graceful-fs: 4.2.11 imurmurhash: 0.1.4 signal-exit: 3.0.7 @@ -12336,6 +12637,8 @@ snapshots: xmlchars@2.2.0: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -12346,10 +12649,29 @@ snapshots: yaml@2.2.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/src/components/Counter/Counter.module.css b/src/components/Counter/Counter.module.css index 3e16459..dc20593 100644 --- a/src/components/Counter/Counter.module.css +++ b/src/components/Counter/Counter.module.css @@ -1,6 +1,6 @@ .counter { border: 1px solid lightgray; - margin: 36px 24px; + margin-bottom: 24px; padding: 24px; text-align: center; width: 240px; diff --git a/src/components/Counter/Counter.spec.tsx b/src/components/Counter/Counter.spec.tsx index b704760..ddd5dc5 100644 --- a/src/components/Counter/Counter.spec.tsx +++ b/src/components/Counter/Counter.spec.tsx @@ -3,7 +3,7 @@ import {Provider} from 'react-redux'; import {render, fireEvent} from '@testing-library/react'; import configureStore from 'redux-mock-store'; -import {Actions} from '@/features/counter/actionTypes'; +import {Actions} from '@/src/features/counter/actionTypes'; import Counter from './Counter'; diff --git a/src/components/Counter/Counter.tsx b/src/components/Counter/Counter.tsx index 95de98c..3a30479 100644 --- a/src/components/Counter/Counter.tsx +++ b/src/components/Counter/Counter.tsx @@ -1,7 +1,9 @@ +'use client'; + import type {FC} from 'react'; import React from 'react'; -import {useCountValue, useIncrementCounter} from '@/features/counter'; +import {useCountValue, useIncrementCounter} from '@/src/features/counter'; import classes from './Counter.module.css'; diff --git a/src/components/Random/Random.module.css b/src/components/Random/Random.module.css index d19a8a2..a806b74 100644 --- a/src/components/Random/Random.module.css +++ b/src/components/Random/Random.module.css @@ -1,6 +1,6 @@ -.counter { +.random { border: 1px solid lightgray; - margin: 36px 24px; + margin-bottom: 24px; padding: 24px; width: 240px; } diff --git a/src/components/Random/Random.spec.tsx b/src/components/Random/Random.spec.tsx index 84aea0f..16f804e 100644 --- a/src/components/Random/Random.spec.tsx +++ b/src/components/Random/Random.spec.tsx @@ -1,12 +1,12 @@ import React from 'react'; import {Provider} from 'react-redux'; +import type {Store, Action} from 'redux'; import {render, fireEvent, waitFor, screen, waitForElementToBeRemoved} from '@testing-library/react'; -import axios from 'axios'; import configureStore from 'redux-mock-store'; -import {GET_RANDOM_NUMBER} from '@/features/random/actionTypes'; -import {store as realStore} from '@/state/ReduxProvider'; -import {promiseResolverMiddleware} from '@/state/promiseResolverMiddleware'; +import {GET_RANDOM_NUMBER} from '@/src/features/random/actionTypes'; +import {makeStore} from '@/src/state/store'; +import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware'; import Random from './Random'; @@ -14,18 +14,34 @@ import Random from './Random'; * Create mock store * @see https://github.com/dmitry-zaets/redux-mock-store */ -// @ts-expect-error TS2322: Type +// @ts-expect-error TS2322 const mockStore = configureStore([promiseResolverMiddleware]); -jest.mock('axios'); - /* We use these strings to match HTMLElements */ const pristineText = 'Click the button to get random number'; const loadingText = 'Getting number'; const errorText = 'Ups...'; const response = 6; +let realStore: Store; + +const fetchBackup = global.fetch; + +const fetchMock = jest.fn(); + describe('components > Random', () => { + beforeAll(() => { + global.fetch = fetchMock; + }); + + beforeEach(() => { + realStore = makeStore(); + fetchMock.mockClear(); + }); + + afterAll(() => { + global.fetch = fetchBackup; + }); /** * Provide table of values to run tests with * @see https://jestjs.io/docs/en/api#describeeachtablename-fn-timeout @@ -67,11 +83,14 @@ describe('components > Random', () => { it('handles successful request', async () => { /** - * Mock axios successful response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.resolve({data: response})); + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); /** * `getByRole`: @@ -105,11 +124,14 @@ describe('components > Random', () => { it('handles rejected request', async () => { /** - * Mock axios rejected response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.reject(new Error(''))); + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); /** * `getByRole`: @@ -151,11 +173,14 @@ describe('components > Random', () => { }); /** - * Mock axios successful response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.resolve({data: response})); + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); /** * `getByRole`: @@ -182,7 +207,7 @@ describe('components > Random', () => { }); /** Second dispatched action should deliver response from API */ - expect(store.getActions()[1].payload.data).toEqual(response); + expect(store.getActions()[1].payload).toEqual(response); }); it('dispatches an action sequence on rejected request made', async () => { @@ -195,11 +220,14 @@ describe('components > Random', () => { }); /** - * Mock axios rejected response - * @see https://www.robinwieruch.de/axios-jest + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch */ - // @ts-expect-error TS2339: Property mockImplementationOnce does not exist on type - axios.get.mockImplementationOnce(() => Promise.reject(new Error(''))); + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); /** * `getByRole`: diff --git a/src/components/Random/Random.tsx b/src/components/Random/Random.tsx index 27e7b72..fa1a452 100644 --- a/src/components/Random/Random.tsx +++ b/src/components/Random/Random.tsx @@ -1,6 +1,8 @@ +'use client'; + import React from 'react'; -import {useGetRandomNumberQuery, useRandomNumber, useLoadingState} from '@/features/random'; +import {useGetRandomNumberQuery, useRandomNumber, useLoadingState} from '@/src/features/random'; import classes from './Random.module.css'; @@ -18,7 +20,7 @@ const Random = () => { const isPristine = !isLoading && !hasError && !isFulfilled; return ( -
+

Async Random

+
+ Total value: {count} +
+
+ ); +}; diff --git a/templates/Component/index.ts b/templates/Component/index.ts new file mode 100644 index 0000000..9347b65 --- /dev/null +++ b/templates/Component/index.ts @@ -0,0 +1 @@ +export {TemplateName} from './TemplateName'; diff --git a/templates/Feature/README.md b/templates/Feature/README.md new file mode 100644 index 0000000..a014eb5 --- /dev/null +++ b/templates/Feature/README.md @@ -0,0 +1,44 @@ +## TemplateName + +Tba + +## Selectors + +### `useTemplateNameLoadingState` + +Returns request loading state from the store. + +```javascript +import {useTemplateNameLoadingState} from 'features/templateName'; + +// Needs to be run from inside React component or other hook. +const {isLoading, hasError, isFulfilled} = useTemplateNameLoadingState(); +``` + +### `useTemplateName` + +Returns random number value from the store + +```javascript +import {useTemplateName} from 'features/templateName'; + +// Needs to be run from inside React component or other hook. +const number = useTemplateName(); +``` + +## Action creators + +### `useGetTemplateNameQuery` + +Performs AJAX query to get TemplateName. + +```javascript +import {useGetTemplateNameQuery} from 'features/templateName'; + +// Needs to be run from inside React component or other hook. +const getTemplateName = useGetTemplateNameQuery(); +const handleClick = () => { + getTemplateName(); +} +``` + diff --git a/templates/Feature/TemplateNameReducer.spec.tsx b/templates/Feature/TemplateNameReducer.spec.tsx new file mode 100644 index 0000000..9542aa4 --- /dev/null +++ b/templates/Feature/TemplateNameReducer.spec.tsx @@ -0,0 +1,42 @@ +import {Actions} from './actionTypes'; +import {TemplateNameReducer} from './TemplateNameReducer'; + +describe('features > random > TemplateNameReducer', () => { + it('returns initial state, if non matched action is dispatched', () => { + const initialState = { + isLoading: false, + hasError: false, + isFulfilled: false, + }; + + const action = { + type: Actions.GET_TEMPLATE_NAME, + payload: 0, + }; + + expect(TemplateNameReducer(initialState, action)).toBe(initialState); + }); + + /** + * Provide table of values to run test case against + * @see https://jestjs.io/docs/en/api#testeachtablename-fn-timeout + */ + it.each([ + [Actions.GET_TEMPLATE_NAME_FULFILLED], + [Actions.GET_TEMPLATE_NAME_PENDING], + [Actions.GET_TEMPLATE_NAME_REJECTED], + ])(`updates state according to dispatched action`, actionType => { + const initialState = { + value: 0, + }; + + const payload = actionType === Actions.GET_TEMPLATE_NAME_FULFILLED ? 1 : undefined; + + const action = { + type: actionType as keyof typeof Actions, + payload, + }; + + expect(TemplateNameReducer(initialState, action)).toMatchSnapshot(); + }); +}); diff --git a/templates/Feature/TemplateNameReducer.ts b/templates/Feature/TemplateNameReducer.ts new file mode 100644 index 0000000..741c148 --- /dev/null +++ b/templates/Feature/TemplateNameReducer.ts @@ -0,0 +1,52 @@ +import {Actions} from './actionTypes'; + +export type State = { + value?: number; + isLoading?: boolean; + hasError?: boolean; + isFulfilled?: boolean; +}; + +const initialState = { + templateName: undefined, + isLoading: false, + hasError: false, + isFulfilled: false, +} as State; + +export type Action = { + type: keyof typeof Actions; + payload?: number; +}; + +export const TemplateNameReducer = (state = initialState, action: Action): State => { + switch (action.type) { + case Actions.GET_TEMPLATE_NAME_PENDING: + return { + ...state, + isFulfilled: false, + isLoading: true, + hasError: false, + value: undefined, + }; + + case Actions.GET_TEMPLATE_NAME_FULFILLED: + return { + isFulfilled: true, + isLoading: false, + hasError: false, + value: action?.payload, + }; + + case Actions.GET_TEMPLATE_NAME_REJECTED: + return { + isFulfilled: false, + isLoading: false, + hasError: true, + value: undefined, + }; + + default: + return state; + } +}; diff --git a/templates/Feature/actionTypes.ts b/templates/Feature/actionTypes.ts new file mode 100644 index 0000000..7ec8721 --- /dev/null +++ b/templates/Feature/actionTypes.ts @@ -0,0 +1,6 @@ +export enum Actions { + GET_TEMPLATE_NAME = 'GET_TEMPLATE_NAME', + GET_TEMPLATE_NAME_REJECTED = 'GET_TEMPLATE_NAME_REJECTED', + GET_TEMPLATE_NAME_FULFILLED = 'GET_TEMPLATE_NAME_FULFILLED', + GET_TEMPLATE_NAME_PENDING = 'GET_TEMPLATE_NAME_PENDING', +} diff --git a/templates/Feature/index.ts b/templates/Feature/index.ts new file mode 100644 index 0000000..d104707 --- /dev/null +++ b/templates/Feature/index.ts @@ -0,0 +1,3 @@ +export {TemplateNameReducer} from './TemplateNameReducer'; +export {useTemplateName, useTemplateNameLoadingState} from './selectors'; +export {useGetTemplateNameQuery} from './useGetTemplateNameQuery'; diff --git a/templates/Feature/selectors.spec.tsx b/templates/Feature/selectors.spec.tsx new file mode 100644 index 0000000..b870c22 --- /dev/null +++ b/templates/Feature/selectors.spec.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import configureStore from 'redux-mock-store'; +import {renderHook} from '@testing-library/react'; + +import {useTemplateNameLoadingState, useTemplateName} from './selectors'; + +describe('features > counter > useTemplateName', () => { + const mockStore = configureStore([]); + + const state = { + templateName: { + value: 42, + }, + }; + + const store = mockStore(state); + + it('returns count value', () => { + /** + * Render hook, using testing-library utility + * @see https://react-hooks-testing-library.com/reference/api#renderhook + */ + const {result} = renderHook(() => useTemplateName(), { + wrapper: ({children}) => {children}, + }); + + expect(result.current).toBe(state.templateName.value); + }); +}); + +describe('features > counter > useTemplateNameLoadingState', () => { + const mockStore = configureStore([]); + + const state = { + templateName: { + isLoading: true, + hasError: true, + isFulfilled: true, + foo: 'bar', + }, + }; + + const store = mockStore(state); + + it('returns count value', () => { + /** + * Render hook, using testing-library utility + * @see https://react-hooks-testing-library.com/reference/api#renderhook + */ + const {result} = renderHook(() => useTemplateNameLoadingState(), { + wrapper: ({children}) => {children}, + }); + + /* We expect hook to return certain values from the state, but not all state */ + expect(result.current).not.toBe(state.templateName); + expect(state.templateName).toMatchObject(result.current); + }); +}); diff --git a/templates/Feature/selectors.ts b/templates/Feature/selectors.ts new file mode 100644 index 0000000..0ab16e2 --- /dev/null +++ b/templates/Feature/selectors.ts @@ -0,0 +1,16 @@ +import {useSelector} from 'react-redux'; + +import type {State} from './TemplateNameReducer'; + +/** + * Custom React Hooks to get random.org API loading state and response from the state. + * + * @see https://reactjs.org/docs/hooks-custom.html + */ +export const useTemplateNameLoadingState = () => { + const {isLoading, hasError, isFulfilled} = useSelector<{templateName: State}, State>(state => state.templateName); + return {isLoading, hasError, isFulfilled}; +}; + +export const useTemplateName = () => + useSelector<{templateName: State}, number | undefined>(state => state.templateName.value); diff --git a/templates/Feature/useGetTemplateNameQuery.spec.tsx b/templates/Feature/useGetTemplateNameQuery.spec.tsx new file mode 100644 index 0000000..8753ef9 --- /dev/null +++ b/templates/Feature/useGetTemplateNameQuery.spec.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import configureStore from 'redux-mock-store'; +import {waitFor, renderHook} from '@testing-library/react'; + +import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware'; + +import {Actions} from './actionTypes'; +import {useGetTemplateNameQuery} from './useGetTemplateNameQuery'; + +const fetchBackup = global.fetch; + +const fetchMock = jest.fn(); + +/** Create mock store with middlewares */ +// @ts-expect-error TS2322 +const mockStore = configureStore([promiseResolverMiddleware]); + +const store = mockStore({ + random: { + isLoading: false, + hasError: false, + isFulfilled: false, + }, +}); + +describe('features > counter > useGetTemplateNameQuery', () => { + beforeAll(() => { + global.fetch = fetchMock; + }); + + beforeEach(() => { + fetchMock.mockClear(); + }); + + afterAll(() => { + global.fetch = fetchBackup; + }); + + it('returns function', () => { + /** + * Render hook, using testing-library utility + * @see https://react-hooks-testing-library.com/reference/api#renderhook + */ + const {result} = renderHook(() => useGetTemplateNameQuery(), { + wrapper: ({children}) => {children}, + }); + + expect(result.current).toBeInstanceOf(Function); + }); + + describe('gets number', () => { + afterEach(() => { + store.clearActions(); + }); + + /** Note that tests functions are async */ + it(`handles successful API query`, async () => { + const {result} = renderHook(() => useGetTemplateNameQuery(), { + wrapper: ({children}) => {children}, + }); + + /** Mock response from API */ + const response = 6; + + /** + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); + + /** + * Wait until async action finishes + */ + await result.current(); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: Actions.GET_TEMPLATE_NAME_PENDING, + }); + + await waitFor(() => { + /** Second dispatched action should have _FULFILLED suffix */ + expect(store.getActions()[1].type).toEqual(Actions.GET_TEMPLATE_NAME_FULFILLED); + /** Second dispatched action should deliver response from API */ + expect(store.getActions()[1].payload).toEqual(response); + }); + }); + + it(`handles rejected API query`, async () => { + const {result} = renderHook(() => useGetTemplateNameQuery(), { + wrapper: ({children}) => {children}, + }); + + /** + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); + + /** + * Wait until async action finishes + */ + await result.current(); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: Actions.GET_TEMPLATE_NAME_PENDING, + }); + + await waitFor(() => { + /** Second dispatched action should have _REJECTED suffix */ + expect(store.getActions()[1].type).toEqual(Actions.GET_TEMPLATE_NAME_REJECTED); + }); + }); + }); +}); diff --git a/templates/Feature/useGetTemplateNameQuery.ts b/templates/Feature/useGetTemplateNameQuery.ts new file mode 100644 index 0000000..c4f88d6 --- /dev/null +++ b/templates/Feature/useGetTemplateNameQuery.ts @@ -0,0 +1,18 @@ +import {useCallback} from 'react'; +import {useDispatch} from 'react-redux'; + +import {Actions} from './actionTypes'; + +export const useGetTemplateNameQuery = () => { + const dispatch = useDispatch(); + return useCallback( + () => + dispatch({ + type: Actions.GET_TEMPLATE_NAME, + payload: fetch('https://www.random.org', { + method: 'GET', + }), + }), + [dispatch] + ); +}; diff --git a/templates/Loading/TemplateName.module.css b/templates/Loading/TemplateName.module.css new file mode 100644 index 0000000..bee4745 --- /dev/null +++ b/templates/Loading/TemplateName.module.css @@ -0,0 +1,37 @@ +.templateName { + border: 1px solid lightgray; + margin-bottom: 24px; + padding: 24px; + width: 240px; +} + +.header { + font-size: 24px; + font-weight: normal; + margin: 0 0 12px; + text-align: center; +} + +.button { + background: lightseagreen; + border: none; + border-radius: 5px; + color: white; + cursor: pointer; + display: block; + font-size: 16px; + margin: 0 auto 24px; + padding: 12px 24px; + text-shadow: 1px 1px 1px rgb(0 0 0 / 50%); +} + +.button:disabled { + background: lightgray; + cursor: not-allowed; +} + +.button:active:not(:disabled) { + left: 1px; + position: relative; + top: 1px; +} diff --git a/templates/Loading/TemplateName.spec.tsx b/templates/Loading/TemplateName.spec.tsx new file mode 100644 index 0000000..5c5606f --- /dev/null +++ b/templates/Loading/TemplateName.spec.tsx @@ -0,0 +1,256 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import type {Store, Action} from 'redux'; +import {render, fireEvent, waitFor, screen, waitForElementToBeRemoved} from '@testing-library/react'; +import configureStore from 'redux-mock-store'; + +import {GET_RANDOM_NUMBER} from '@/src/features/random/actionTypes'; +import {makeStore} from '@/src/state/store'; +import {promiseResolverMiddleware} from '@/src/state/promiseResolverMiddleware'; + +import {TemplateName} from './TemplateName'; + +/** + * Create mock store + * @see https://github.com/dmitry-zaets/redux-mock-store + */ +// @ts-expect-error TS2322 +const mockStore = configureStore([promiseResolverMiddleware]); + +/* We use these strings to match HTMLElements */ +const pristineText = 'Click the button to get random number'; +const loadingText = 'Getting number'; +const errorText = 'Ups...'; +const response = 6; + +let realStore: Store; + +const fetchBackup = global.fetch; + +const fetchMock = jest.fn(); + +describe('components > TemplateName', () => { + beforeAll(() => { + global.fetch = fetchMock; + }); + + beforeEach(() => { + realStore = makeStore(); + fetchMock.mockClear(); + }); + + afterAll(() => { + global.fetch = fetchBackup; + }); + /** + * Provide table of values to run tests with + * @see https://jestjs.io/docs/en/api#describeeachtablename-fn-timeout + */ + describe.each` + isLoading | hasError | isFulfilled + ${false} | ${false} | ${false} + ${true} | ${false} | ${false} + ${false} | ${true} | ${false} + ${false} | ${false} | ${true} + `('renders different store states', ({isLoading, hasError, isFulfilled}) => { + it(`when isLoading === ${isLoading} && hasError === ${hasError} && isFulfilled === ${isFulfilled}`, () => { + const store = mockStore({ + random: { + isLoading, + hasError, + isFulfilled, + number: isFulfilled ? 1 : undefined, + }, + }); + + /** + * `asFragment`: + * @see https://testing-library.com/docs/react-testing-library/api#asfragment + * `wrapper`: + * @see https://testing-library.com/docs/react-testing-library/api#wrapper + */ + const {asFragment} = render(, { + wrapper: ({children}) => {children}, + }); + + /** + * Basic snapshot test to check, if rendered component + * matches expected footprint. + */ + expect(asFragment()).toMatchSnapshot(); + }); + }); + + it('handles successful request', async () => { + /** + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {asFragment, getByRole} = render(, { + wrapper: ({children}) => ( + /* We use real store here, to get action through */ + {children} + ), + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** Check that initial message has changed to loading. */ + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).toBeInTheDocument(); + + /** Check that loading message has changed to success. */ + await waitForElementToBeRemoved(() => screen.queryByText(loadingText)); + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).not.toBeInTheDocument(); + expect(screen.queryByText(response)).toBeInTheDocument(); + }); + + it('handles rejected request', async () => { + /** + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {asFragment, getByRole} = render(, { + wrapper: ({children}) => ( + /* We use real store here, to get action through */ + {children} + ), + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** Check that initial message has changed to loading. */ + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).toBeInTheDocument(); + + /** Check that loading message has changed to error. */ + await waitForElementToBeRemoved(() => screen.queryByText(loadingText)); + expect(asFragment()).toMatchSnapshot(); + expect(screen.queryByText(pristineText)).not.toBeInTheDocument(); + expect(screen.queryByText(loadingText)).not.toBeInTheDocument(); + expect(screen.queryByText(errorText)).toBeInTheDocument(); + }); + + it('dispatches an action sequence on successful request made', async () => { + const store = mockStore({ + random: { + isLoading: false, + hasError: false, + isFulfilled: false, + }, + }); + + /** + * Mock fetch successful response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(response), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {getByRole} = render(, { + wrapper: ({children}) => {children}, + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: `${GET_RANDOM_NUMBER}_PENDING`, + }); + + await waitFor(() => { + /** Second dispatched action should have _FULFILLED suffix */ + expect(store.getActions()[1].type).toEqual(`${GET_RANDOM_NUMBER}_FULFILLED`); + }); + + /** Second dispatched action should deliver response from API */ + expect(store.getActions()[1].payload).toEqual(response); + }); + + it('dispatches an action sequence on rejected request made', async () => { + const store = mockStore({ + random: { + isLoading: false, + hasError: false, + isFulfilled: false, + }, + }); + + /** + * Mock fetch rejected response + * @see https://developer.mozilla.org/en-US/docs/Web/API/fetch + */ + fetchMock.mockImplementationOnce(() => + Promise.reject({ + json: () => Promise.reject(new Error('')), + }) + ); + + /** + * `getByRole`: + * @see https://testing-library.com/docs/dom-testing-library/api-queries#byrole + */ + const {getByRole} = render(, { + wrapper: ({children}) => {children}, + }); + + /** + * Search for the button and make testing library click on it + * @see https://testing-library.com/docs/react-testing-library/cheatsheet#events + */ + fireEvent.click(getByRole('button')); + + /** First dispatched action should have _PENDING suffix */ + expect(store.getActions()[0]).toEqual({ + type: `${GET_RANDOM_NUMBER}_PENDING`, + }); + + await waitFor(() => { + /** Second dispatched action should have _REJECTED suffix */ + expect(store.getActions()[1].type).toEqual(`${GET_RANDOM_NUMBER}_REJECTED`); + }); + }); +}); diff --git a/templates/Loading/TemplateName.tsx b/templates/Loading/TemplateName.tsx new file mode 100644 index 0000000..6e546ab --- /dev/null +++ b/templates/Loading/TemplateName.tsx @@ -0,0 +1,38 @@ +'use client'; + +import React from 'react'; + +import {useGetRandomNumberQuery, useRandomNumber, useLoadingState} from '@/src/features/random'; + +import classes from './TemplateName.module.css'; + +export const TemplateName = () => { + /** Loading state of random.org request from Redux store */ + const {isLoading, hasError, isFulfilled} = useLoadingState(); + + /** Random number value */ + const number = useRandomNumber(); + + /** Create incrementCounter action, using custom hook from feature */ + const getNumber = useGetRandomNumberQuery(); + + /** Define pristine state condition, when user didn't do any actions */ + const isPristine = !isLoading && !hasError && !isFulfilled; + + return ( +
+

Async Random

+ + {isPristine &&
Click the button to get random number
} + {isLoading &&
Getting number
} + {isFulfilled && ( +
+ Number from random.org: {number} +
+ )} + {hasError &&
Ups...
} +
+ ); +}; diff --git a/templates/Loading/index.ts b/templates/Loading/index.ts new file mode 100644 index 0000000..9347b65 --- /dev/null +++ b/templates/Loading/index.ts @@ -0,0 +1 @@ +export {TemplateName} from './TemplateName'; diff --git a/tsconfig.json b/tsconfig.json index f8078e7..6b8bf07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,9 +21,14 @@ "baseUrl": ".", "paths": { "@/*": [ - "./src/*" + "./*" ] - } + }, + "plugins": [ + { + "name": "next" + } + ] }, "include": [ "next-env.d.ts", @@ -34,9 +39,12 @@ "**/*.stories.ts", "*.config.ts", "src", + "app", ".eslintrc.cjs", "templates/**/*.ts", - "templates/**/*.tsx" + "templates/**/*.tsx", + "build/types/**/*.ts", + ".next/types/**/*.ts" ], "exclude": [ "node_modules"