├── .env
├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── README.md
├── index.html
├── jest.config.cjs
├── package.json
├── public
├── .htaccess
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── RootComponent.tsx
├── __tests__
│ ├── components
│ │ └── DateDisplay.test.tsx
│ └── pages
│ │ ├── HomePage.test.tsx
│ │ └── NotFoundPage.test.tsx
├── components
│ └── DateDisplay.tsx
├── index.tsx
├── pages
│ ├── HomePage.tsx
│ └── NotFoundPage.tsx
├── resources
│ ├── api-constants.ts
│ └── routes-constants.ts
├── serviceWorker.ts
├── setupTests.ts
├── store
│ ├── actions
│ │ ├── data.ts
│ │ └── thunkActions.ts
│ └── reducers
│ │ ├── data.ts
│ │ └── store.ts
├── styles
│ ├── _mixins.sass
│ ├── _variables.sass
│ └── main.sass
├── types
│ └── reducers.ts
├── utility
│ ├── customAxios.ts
│ └── functions.ts
└── vite-env.d.ts
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | SERVER_OPEN_BROWSER=true
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | # Ignore artifacts:
3 | build/
4 | coverage/
5 |
6 | vite-env.d.ts
7 | vite.config.ts
8 | jest.config.js
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4 | plugins: ['@typescript-eslint', 'react-hooks', 'prettier'],
5 | parserOptions: {
6 | ecmaVersion: 2018,
7 | sourceType: 'module',
8 | ecmaFeatures: {
9 | jsx: true
10 | }
11 | },
12 | env: {
13 | browser: true,
14 | node: true,
15 | es6: true,
16 | jest: true
17 | },
18 | rules: {
19 | '@typescript-eslint/naming-convention': [
20 | 'warn',
21 | {
22 | selector: 'parameter',
23 | format: ['camelCase'],
24 | trailingUnderscore: 'allowSingleOrDouble'
25 | },
26 | {
27 | selector: 'class',
28 | format: ['PascalCase']
29 | },
30 | {
31 | selector: 'enum',
32 | format: ['PascalCase', 'UPPER_CASE']
33 | },
34 | {
35 | selector: 'enumMember',
36 | format: ['PascalCase', 'UPPER_CASE']
37 | },
38 | {
39 | selector: 'interface',
40 | format: ['PascalCase']
41 | },
42 | {
43 | selector: 'typeAlias',
44 | format: ['PascalCase']
45 | },
46 | {
47 | selector: 'typeParameter',
48 | format: ['PascalCase']
49 | }
50 | ],
51 | 'react-hooks/rules-of-hooks': 'warn',
52 | 'react-hooks/exhaustive-deps': 'warn'
53 | },
54 | settings: {
55 | react: {
56 | version: 'detect'
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 |
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 |
11 | /coverage
12 |
13 | # production
14 |
15 | /build
16 |
17 | # misc
18 |
19 | .eslintcache
20 | .DS_Store
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log\*
29 |
30 | yarn.lock
31 | package-lock.json
32 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 160,
3 | "tabWidth": 4,
4 | "singleQuote": true,
5 | "semi": false,
6 | "trailingComma": "none",
7 | "bracketSpacing": true
8 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
New React App
2 |
3 | # Usage
4 |
5 | To clone and use this template type the following commands:
6 |
7 | ```sh
8 | npx degit chrisuser/vite-complete-react-app my-app
9 | ```
10 |
11 | ```sh
12 | cd my-app
13 | ```
14 |
15 | Then, based on your package manager:
16 |
17 | ## npm
18 |
19 | ```sh
20 | npm install
21 | ```
22 |
23 | ```sh
24 | npm run dev
25 | ```
26 |
27 | ## yarn
28 |
29 | ```sh
30 | yarn
31 | ```
32 |
33 | ```sh
34 | yarn dev
35 | ```
36 |
37 |
38 |
39 | > [!TIP]
40 | > Remember to update the project name inside the `package.json` file.
41 |
42 |
43 |
44 | ## ⚗️ Technologies list
45 |
46 | - [TypeScript](https://www.typescriptlang.org/)
47 | - [Sass](https://sass-lang.com/)
48 | - [Redux Toolkit](https://redux-toolkit.js.org/)
49 | - [Router](https://reactrouter.com/)
50 | - [Axios](https://axios-http.com/)
51 | - [Moment](https://momentjs.com/)
52 | - [ESlint](https://eslint.org/) & [Prettier](https://prettier.io/)
53 |
54 | ---
55 |
56 |
57 |
58 | > [!TIP]
59 | > After cloning the repo you can delete all the previous text for a cleaner README.
60 |
61 |
62 |
63 | This is a blank README file that you can customize at your needs.\
64 | Describe your project, how it works and how to contribute to it.
65 |
66 |
67 |
68 | # 🚀 Available Scripts
69 |
70 | In the project directory, you can run:
71 |
72 |
73 |
74 | ## ⚡️ start
75 |
76 | ```
77 | npm start
78 | ```
79 |
80 | or
81 |
82 | ```
83 | yarn start
84 | ```
85 |
86 | Runs the app in the development mode.\
87 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
88 |
89 |
90 |
91 | ## 🧪 test
92 |
93 | ```
94 | npm test
95 | ```
96 |
97 | or
98 |
99 | ```
100 | yarn test
101 | ```
102 |
103 | Launches the test runner in the interactive watch mode.
104 |
105 |
106 |
107 | ## 🦾 build
108 |
109 | ```
110 | npm build
111 | ```
112 |
113 | or
114 |
115 | ```
116 | yarn build
117 | ```
118 |
119 | Builds the app for production to the `build` folder.\
120 | It correctly bundles React in production mode and optimizes the build for the best performance.
121 |
122 | The build is minified and the filenames include the hashes.
123 |
124 |
125 |
126 | ## 🧶 lint
127 |
128 | ```
129 | npm lint
130 | ```
131 |
132 | or
133 |
134 | ```
135 | yarn lint
136 | ```
137 |
138 | Creates a `.eslintcache` file in which ESLint cache is stored. Running this command can dramatically improve ESLint's running time by ensuring that only changed files are linted.
139 |
140 |
141 |
142 | ## 🎯 format
143 |
144 | ```
145 | npm format
146 | ```
147 |
148 | or
149 |
150 | ```
151 | yarn format
152 | ```
153 |
154 | Checks if your files are formatted. This command will output a human-friendly message and a list of unformatted files, if any.
155 |
156 |
157 |
158 | # 🧬 Project structure
159 |
160 | This is the structure of the files in the project:
161 |
162 | ```sh
163 | │
164 | ├── public # public files (favicon, .htaccess, manifest, ...)
165 | ├── src # source files
166 | │ ├── __tests__ # all test files
167 | │ ├── components
168 | │ ├── pages
169 | │ ├── resources # images, constants and other static resources
170 | │ ├── store # Redux store
171 | │ │ ├── actions # store's actions
172 | │ │ └── reducers # store's reducers
173 | │ ├── styles
174 | │ ├── types # data interfaces
175 | │ ├── utility # utilities functions and custom components
176 | │ ├── App.tsx
177 | │ ├── index.tsx
178 | │ ├── RootComponent.tsx # React component with all the routes
179 | │ ├── serviceWorker.ts
180 | │ ├── setupTests.ts
181 | │ └── vite-env.d.ts
182 | ├── .env
183 | ├── .eslintignore
184 | ├── .eslintrc.js
185 | ├── .gitignore
186 | ├── .prettierrc
187 | ├── index.html
188 | ├── jest.config.cjs
189 | ├── package.json
190 | ├── README.md
191 | ├── tsconfig.json
192 | └── vite.config.json
193 | ```
194 |
195 | # 📖 Learn More
196 |
197 | You can learn more in the [Vite documentation](https://vitejs.dev/guide/).
198 |
199 | To learn React, check out the [React documentation](https://reactjs.org/).
200 |
201 | #
202 |
203 | Bootstrapped with Vite.
204 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | React App
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['/src'],
3 | transform: {
4 | '^.+\\.tsx$': 'ts-jest',
5 | '^.+\\.ts$': 'ts-jest'
6 | },
7 | testRegex: '(/__tests__/.*.(test|spec)).(jsx?|tsx?)$',
8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
9 | collectCoverage: true,
10 | collectCoverageFrom: ['/src/**/*.{ts,tsx}'],
11 | coverageDirectory: '/coverage/',
12 | coveragePathIgnorePatterns: ['(tests/.*.mock).(jsx?|tsx?)$', '(.*).d.ts$'],
13 | moduleNameMapper: {
14 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$': 'identity-obj-proxy'
15 | },
16 | verbose: true,
17 | testTimeout: 30000
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-complete-react-app",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "author": {
6 | "name": "Cristiano Raimondi",
7 | "url": "https://github.com/chrisuser"
8 | },
9 | "dependencies": {
10 | "@reduxjs/toolkit": "^2.5.0",
11 | "@testing-library/jest-dom": "^6.6.3",
12 | "@testing-library/react": "^16.1.0",
13 | "@testing-library/user-event": "^14.5.2",
14 | "axios": "^1.7.9",
15 | "eslint-plugin-prettier": "^5.2.1",
16 | "moment": "^2.30.1",
17 | "react": "^19.0.0",
18 | "react-dom": "^19.0.0",
19 | "react-redux": "^9.2.0",
20 | "react-router": "^7.1.1",
21 | "redux-persist": "^6.0.0",
22 | "sass": "^1.83.0",
23 | "sass-loader": "^16.0.4"
24 | },
25 | "scripts": {
26 | "start": "vite --port 3000 --open",
27 | "dev": "vite --port 3000 --open",
28 | "build": "vite build",
29 | "serve": "vite preview --open",
30 | "test": "jest",
31 | "test:coverage": "jest --silent --watchAll=false --coverage",
32 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
33 | "format": "prettier --check ."
34 | },
35 | "eslintConfig": {
36 | "extends": "react-app"
37 | },
38 | "browserslist": [
39 | "defaults",
40 | "not IE 11"
41 | ],
42 | "devDependencies": {
43 | "@testing-library/dom": "^10.4.0",
44 | "@types/jest": "^29.5.14",
45 | "@types/node": "^22.10.2",
46 | "@types/react": "^19.0.2",
47 | "@types/react-dom": "^19.0.2",
48 | "@types/react-redux": "^7.1.34",
49 | "@typescript-eslint/eslint-plugin": "^8.18.2",
50 | "@typescript-eslint/parser": "^8.18.2",
51 | "@vitejs/plugin-react": "^4.3.4",
52 | "eslint": "^8.56.0",
53 | "eslint-plugin-react-hooks": "^5.1.0",
54 | "identity-obj-proxy": "^3.0.0",
55 | "jest": "^29.7.0",
56 | "jest-environment-jsdom": "^29.7.0",
57 | "prettier": "^3.4.2",
58 | "ts-jest": "^29.2.5",
59 | "typescript": "^5.7.2",
60 | "vite": "^6.0.6",
61 | "vite-plugin-env-compatible": "^2.0.1",
62 | "vite-plugin-html": "^3.2.2",
63 | "vitest": "^2.1.8"
64 | },
65 | "homepage": "."
66 | }
67 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Options -MultiViews
3 |
4 | RewriteEngine On
5 |
6 | RewriteCond %{REQUEST_FILENAME} !-f
7 | RewriteRule ^ index.html [QSA,L]
8 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisUser/vite-complete-react-app/eb42f81b768f6729b8f61c84722bd42be6d5fcae/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisUser/vite-complete-react-app/eb42f81b768f6729b8f61c84722bd42be6d5fcae/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisUser/vite-complete-react-app/eb42f81b768f6729b8f61c84722bd42be6d5fcae/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Provider } from 'react-redux'
3 | import { PersistGate } from 'redux-persist/integration/react'
4 | import RootComponent from './RootComponent'
5 | import { persistor, store } from './store/reducers/store'
6 |
7 | const App: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default App
18 |
--------------------------------------------------------------------------------
/src/RootComponent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { BrowserRouter as Router, Route, Routes } from 'react-router'
3 | import HomePage from './pages/HomePage'
4 | import NotFoundPage from './pages/NotFoundPage'
5 | import { ROUTES } from './resources/routes-constants'
6 | import './styles/main.sass'
7 |
8 | const RootComponent: React.FC = () => {
9 | return (
10 |
11 |
12 | } />
13 | } />
14 |
15 |
16 | )
17 | }
18 |
19 | export default RootComponent
20 |
--------------------------------------------------------------------------------
/src/__tests__/components/DateDisplay.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import { render, screen, waitFor } from '@testing-library/react'
5 | import '@testing-library/jest-dom'
6 | import DateDisplay from '../../components/DateDisplay'
7 |
8 | test('renders current date', () => {
9 | render()
10 | ;async () => {
11 | const timeFormat = screen.getByText(/GMT/i)
12 | await waitFor(() => expect(timeFormat).toBeInTheDocument())
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/src/__tests__/pages/HomePage.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import { render, screen } from '@testing-library/react'
5 | import '@testing-library/jest-dom'
6 | import HomePage from '../../pages/HomePage'
7 |
8 | test('renders hello world message', () => {
9 | render()
10 | const greetings = screen.getByText(/Hello world/i)
11 | expect(greetings).toBeInTheDocument()
12 | })
13 |
--------------------------------------------------------------------------------
/src/__tests__/pages/NotFoundPage.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import { render, screen } from '@testing-library/react'
5 | import { TextEncoder, TextDecoder } from 'util'
6 |
7 | Object.assign(global, { TextDecoder, TextEncoder })
8 |
9 | import '@testing-library/jest-dom'
10 | import { BrowserRouter as Router } from 'react-router'
11 | import NotFoundPage from '../../pages/NotFoundPage'
12 |
13 | test('renders error message', () => {
14 | render(
15 |
16 |
17 |
18 | )
19 | const errorMessage = screen.getByText(/Oops 404!/i)
20 | expect(errorMessage).toBeInTheDocument()
21 | })
22 |
--------------------------------------------------------------------------------
/src/components/DateDisplay.tsx:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 | import React, { useEffect, useState } from 'react'
3 |
4 | const DateDisplay: React.FC = () => {
5 | const [date, setDate] = useState('')
6 |
7 | /**
8 | * On component render sets the date state to current date and time
9 | */
10 | useEffect(() => {
11 | const interval = setInterval(() => {
12 | setDate(moment().toDate().toString())
13 | }, 1000)
14 | return () => clearInterval(interval)
15 | }, [])
16 |
17 | return (
18 |
19 | {date}
20 |
21 | )
22 | }
23 |
24 | export default DateDisplay
25 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App'
4 | import * as serviceWorker from './serviceWorker'
5 |
6 | const root = createRoot(document.getElementById('root')!) // createRoot(container!) if you use TypeScript
7 | root.render()
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister()
13 |
--------------------------------------------------------------------------------
/src/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DateDisplay from '../components/DateDisplay'
3 |
4 | const HomePage: React.FC = () => {
5 | return (
6 |
7 |
Hello world!
8 |
9 |
10 | )
11 | }
12 |
13 | export default HomePage
14 |
--------------------------------------------------------------------------------
/src/pages/NotFoundPage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useNavigate } from 'react-router'
3 | import { ROUTES } from '../resources/routes-constants'
4 |
5 | const NotFoundPage: React.FC = () => {
6 | const navigate = useNavigate()
7 |
8 | /**
9 | * Call this function to redirect the user to the homepage.
10 | */
11 | const redirectToHomePage = () => {
12 | navigate(ROUTES.HOMEPAGE_ROUTE)
13 | }
14 |
15 | return (
16 |
17 |
Oops 404!
18 | redirectToHomePage()}>
19 | Homepage
20 |
21 |
22 | )
23 | }
24 |
25 | export default NotFoundPage
26 |
--------------------------------------------------------------------------------
/src/resources/api-constants.ts:
--------------------------------------------------------------------------------
1 | const baseUrl = 'http://exampleurl'
2 |
3 | export const getData = (userId: number): string => {
4 | return baseUrl + '/data/' + userId
5 | }
--------------------------------------------------------------------------------
/src/resources/routes-constants.ts:
--------------------------------------------------------------------------------
1 | export const ROUTES = {
2 | HOMEPAGE_ROUTE: '/'
3 | }
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This lets the app load faster on subsequent visits in production, and gives
2 | // it offline capabilities. However, it also means that developers (and users)
3 | // will only see deployed updates on subsequent visits to a page, after all the
4 | // existing tabs open on the page have been closed, since previously cached
5 | // resources are updated in the background.
6 |
7 | // To learn more about the benefits of this model and instructions on how to
8 | // opt-in, read https://bit.ly/CRA-PWA
9 |
10 | const isLocalhost = Boolean(
11 | window.location.hostname === 'localhost' ||
12 | window.location.hostname === '[::1]' ||
13 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
14 | )
15 |
16 | type Config = {
17 | onSuccess?: (registration: ServiceWorkerRegistration) => void
18 | onUpdate?: (registration: ServiceWorkerRegistration) => void
19 | }
20 |
21 | export const register = (config?: Config): void => {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | const publicUrl = new URL(process.env.PUBLIC_URL || '', window.location.href)
24 | if (publicUrl.origin !== window.location.origin) {
25 | return
26 | }
27 | window.addEventListener('load', () => {
28 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
29 | if (isLocalhost) {
30 | checkValidServiceWorker(swUrl, config)
31 | navigator.serviceWorker.ready.then(() => {
32 | console.log('This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://bit.ly/CRA-PWA')
33 | })
34 | } else {
35 | registerValidSW(swUrl, config)
36 | }
37 | })
38 | }
39 | }
40 |
41 | function registerValidSW(swUrl: string, config?: Config) {
42 | navigator.serviceWorker
43 | .register(swUrl)
44 | .then((registration) => {
45 | registration.onupdatefound = () => {
46 | const installingWorker = registration.installing
47 | if (installingWorker == null) {
48 | return
49 | }
50 | installingWorker.onstatechange = () => {
51 | if (installingWorker.state === 'installed') {
52 | if (navigator.serviceWorker.controller) {
53 | console.log('New content is available and will be used when all ' + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.')
54 | if (config && config.onUpdate) {
55 | config.onUpdate(registration)
56 | }
57 | } else {
58 | console.log('Content is cached for offline use.')
59 | if (config && config.onSuccess) {
60 | config.onSuccess(registration)
61 | }
62 | }
63 | }
64 | }
65 | }
66 | })
67 | .catch((error) => {
68 | console.error('Error during service worker registration:', error)
69 | })
70 | }
71 |
72 | function checkValidServiceWorker(swUrl: string, config?: Config) {
73 | fetch(swUrl, {
74 | headers: { 'Service-Worker': 'script' }
75 | })
76 | .then((response) => {
77 | const contentType = response.headers.get('content-type')
78 | if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
79 | navigator.serviceWorker.ready.then((registration) => {
80 | registration.unregister().then(() => {
81 | window.location.reload()
82 | })
83 | })
84 | } else {
85 | registerValidSW(swUrl, config)
86 | }
87 | })
88 | .catch(() => {
89 | console.log('No internet connection found. App is running in offline mode.')
90 | })
91 | }
92 |
93 | export const unregister = (): void => {
94 | if ('serviceWorker' in navigator) {
95 | navigator.serviceWorker.ready
96 | .then((registration) => {
97 | registration.unregister()
98 | })
99 | .catch((error) => {
100 | console.error(error.message)
101 | })
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import { expect, afterEach } from 'vitest'
2 | import { cleanup } from '@testing-library/react'
3 | import * as matchers from '@testing-library/jest-dom/matchers'
4 |
5 | expect.extend(matchers)
6 | afterEach(cleanup)
7 |
--------------------------------------------------------------------------------
/src/store/actions/data.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '@reduxjs/toolkit'
2 |
3 | export const setContents = createAction('data/setContents')
4 |
--------------------------------------------------------------------------------
/src/store/actions/thunkActions.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { createAsyncThunk } from '@reduxjs/toolkit'
3 | import { AppDispatch } from '../reducers/store'
4 | import { setContents } from './data'
5 |
6 | export const getData = createAsyncThunk('groupedActions/getData', async (string, { dispatch }) => {
7 | try {
8 | const data: any[] = await axios.get(`https://fakeurl.fake/${string}`).then((response) => response.data.results)
9 | dispatch(setContents(data))
10 | } catch (e) {
11 | console.error(e)
12 | throw e
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/src/store/reducers/data.ts:
--------------------------------------------------------------------------------
1 | import { createReducer } from '@reduxjs/toolkit'
2 | import { setContents } from '../actions/data'
3 |
4 | interface DataReducer {
5 | contents: string[]
6 | }
7 |
8 | const initialState: DataReducer = {
9 | contents: []
10 | }
11 |
12 | const dataReducer = createReducer(initialState, (builder) => {
13 | builder.addCase(setContents, (state, action) => {
14 | state.contents = action.payload
15 | })
16 | })
17 |
18 | export default dataReducer
19 |
--------------------------------------------------------------------------------
/src/store/reducers/store.ts:
--------------------------------------------------------------------------------
1 | import { ThunkDispatch, UnknownAction, combineReducers, configureStore } from '@reduxjs/toolkit'
2 | import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist'
3 | import storage from 'redux-persist/lib/storage' // defaults to localStorage
4 | import data from './data'
5 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
6 |
7 | const rootReducer = combineReducers({
8 | data
9 | })
10 |
11 | const persistedReducer = persistReducer(
12 | {
13 | key: 'root',
14 | storage,
15 | whitelist: ['data']
16 | },
17 | rootReducer
18 | )
19 |
20 | export const store = configureStore({
21 | reducer: persistedReducer,
22 | middleware: (getDefaultMiddleware) =>
23 | getDefaultMiddleware({
24 | serializableCheck: {
25 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
26 | }
27 | })
28 | })
29 |
30 | export type RootState = ReturnType
31 | export type AppDispatch = ThunkDispatch
32 |
33 | export const useAppDispatch = () => useDispatch()
34 | export const useAppSelector: TypedUseSelectorHook = useSelector
35 |
36 | export const persistor = persistStore(store)
37 |
--------------------------------------------------------------------------------
/src/styles/_mixins.sass:
--------------------------------------------------------------------------------
1 | $phone-size: 600px
2 | $tablet-size: 992px
3 | $desktop-size: 1200px
4 |
5 | =flexbox($justify, $align, $direction, $wrap)
6 | display: flex
7 | justify-content: $justify
8 | align-items: $align
9 | flex-direction: $direction
10 | flex-wrap: $wrap
11 |
12 | =rectangle($width, $height)
13 | width: $width
14 | height: $height
15 |
16 | =square($size)
17 | +rectangle($size, $size)
18 |
19 | @mixin breakpoint($class)
20 | @if $class == 'desktop'
33 | @media (min-width: $desktop-size)
34 | @content
35 |
--------------------------------------------------------------------------------
/src/styles/_variables.sass:
--------------------------------------------------------------------------------
1 | // Cold colors
2 | $aqua: #7fdbff
3 | $blue: #0074d9
4 | $navy: #001f3f
5 | $teal: #39cccc
6 | $green: #2ecc40
7 | $olive: #3d9970
8 | $lime: #01ff70
9 |
10 | // Warm colors
11 | $yellow: #ffdc00
12 | $orange: #ff851b
13 | $red: #ff4136
14 | $fuchsia: #f012be
15 | $purple: #b10dc9
16 | $maroon: #85144b
17 |
18 | // Grayscale
19 | $white: #ffffff
20 | $silver: #dddddd
21 | $gray: #aaaaaa
22 | $black: #111111
23 |
24 | $main-background-color: $white
25 | $main-text-color: $black
26 | $divider-color: rgba($black, .14)
27 |
28 | $main-text-size: 14px
29 | $main-border-radius: 6px
30 |
31 | $small-space: 16px
32 | $medium-space: 32px
33 | $large-space: 64px
34 |
--------------------------------------------------------------------------------
/src/styles/main.sass:
--------------------------------------------------------------------------------
1 | @use './_variables' as *
2 | @use './_mixins' as *
3 |
4 | *
5 | box-sizing: border-box
6 | outline: none
7 |
8 | html,
9 | body
10 | height: 100%
11 | overflow-x: hidden
12 |
13 | body
14 | position: relative
15 | margin: 0
16 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif
17 | -webkit-font-smoothing: antialiased
18 | -moz-osx-font-smoothing: grayscale
19 | color: $main-text-color
20 | background-color: $main-background-color
21 | font-size: $main-text-size
22 |
23 | code
24 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace
25 |
--------------------------------------------------------------------------------
/src/types/reducers.ts:
--------------------------------------------------------------------------------
1 | export interface ReducerData {
2 | contents: string[]
3 | }
4 |
5 | export type ReduxActionData = {
6 | type: any
7 | payload?: T
8 | }
9 |
10 | export type ReduxAction = (data: T) => ReduxActionData
11 |
--------------------------------------------------------------------------------
/src/utility/customAxios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const CustomAxios = axios.create()
4 |
5 | const toCamelCase: any = (object: any) => {
6 | let transformedObject = object
7 | if (typeof object === 'object' && object !== null) {
8 | if (object instanceof Array) {
9 | transformedObject = object.map(toCamelCase)
10 | } else {
11 | transformedObject = {}
12 | for (const key in object) {
13 | if (object[key] !== undefined) {
14 | const newKey = key.replace(/(_\w)|(-\w)/g, (k) => k[1].toUpperCase())
15 | transformedObject[newKey] = toCamelCase(object[key])
16 | }
17 | }
18 | }
19 | }
20 | return transformedObject
21 | }
22 |
23 | export const toSnackCase: any = (object: any) => {
24 | let transformedObject = object
25 | if (typeof object === 'object' && object !== null) {
26 | if (object instanceof Array) {
27 | transformedObject = object.map(toSnackCase)
28 | } else {
29 | transformedObject = {}
30 | for (const key in object) {
31 | if (object[key] !== undefined) {
32 | const newKey = key
33 | .replace(/\.?([A-Z]+)/g, function (_, y) {
34 | return '_' + y.toLowerCase()
35 | })
36 | .replace(/^_/, '')
37 | transformedObject[newKey] = toSnackCase(object[key])
38 | }
39 | }
40 | }
41 | }
42 | return transformedObject
43 | }
44 |
45 | CustomAxios.interceptors.response.use(
46 | (response) => {
47 | response.data = toCamelCase(response.data)
48 | return response
49 | },
50 | (error) => {
51 | return Promise.reject(error)
52 | }
53 | )
54 |
55 | CustomAxios.interceptors.request.use(
56 | (config) => {
57 | config.data = toSnackCase(config.data)
58 | return config
59 | },
60 | (error) => {
61 | return Promise.reject(error)
62 | }
63 | )
64 |
65 | export default CustomAxios
66 |
--------------------------------------------------------------------------------
/src/utility/functions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This function can be used anywhere in the app to greet the user
3 | * @param userName The user's first name
4 | * @returns A kind greeting message
5 | */
6 | export const sayHello = (userName: string): string => {
7 | return 'Welcome ' + userName + '!'
8 | }
9 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "paths": {
19 | "~/*": ["./src/*"]
20 | },
21 | "baseUrl": "."
22 | },
23 | "include": ["./src"]
24 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { defineConfig, loadEnv } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import envCompatible from 'vite-plugin-env-compatible'
5 | import { createHtmlPlugin } from 'vite-plugin-html'
6 |
7 | const ENV_PREFIX = 'REACT_APP_'
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig(({ mode }) => {
11 | const env = loadEnv(mode, 'env', ENV_PREFIX)
12 |
13 | return {
14 | plugins: [
15 | react(),
16 | envCompatible({ prefix: ENV_PREFIX }),
17 | createHtmlPlugin({
18 | inject: {
19 | data: {
20 | env: {
21 | NODE_ENV: process.env.NODE_ENV,
22 | REACT_APP_CLIENT_TOKEN: process.env.REACT_APP_CLIENT_TOKEN,
23 | REACT_APP_ENV: process.env.REACT_APP_ENV
24 | }
25 | }
26 | },
27 | minify: true
28 | })
29 | ],
30 | test: {
31 | globals: true,
32 | environment: 'jsdom',
33 | setupFiles: ['./src/setupTests.js']
34 | },
35 | resolve: {
36 | alias: {
37 | '~': path.resolve(__dirname, 'src')
38 | }
39 | },
40 | server: {
41 | port: 3000,
42 | open: env.SERVER_OPEN_BROWSER === 'true'
43 | },
44 | build: {
45 | outDir: 'build'
46 | }
47 | }
48 | })
49 |
--------------------------------------------------------------------------------