├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── my-component-library ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── components │ │ ├── counter │ │ │ └── Counter.tsx │ │ ├── index.ts │ │ └── sample-component │ │ │ └── SampleComp.tsx │ ├── index.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── my-js-helpers ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── helpers │ │ ├── index.ts │ │ └── random-id │ │ │ └── random-id.ts │ ├── index.ts │ ├── tests │ │ └── random-id │ │ │ └── randomid.test.ts │ └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts ├── my-react-project ├── .gitignore ├── .prettierignore ├── .prettierrc ├── NOTES.md ├── README.md ├── index.html ├── json-server │ └── db.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── robots.txt │ └── static │ │ └── mock-data │ │ ├── items │ │ └── items.json │ │ └── localization │ │ └── translation │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── fr-FR.json │ │ └── it-IT.json ├── src │ ├── .env.beta │ ├── .env.jsonserver │ ├── .env.localapis │ ├── .env.mock │ ├── .env.production │ ├── App.tsx │ ├── api-client │ │ ├── index.ts │ │ ├── live │ │ │ ├── index.ts │ │ │ ├── items │ │ │ │ └── index.ts │ │ │ └── localization │ │ │ │ └── index.ts │ │ ├── mock │ │ │ ├── index.ts │ │ │ ├── items │ │ │ │ └── index.ts │ │ │ └── localization │ │ │ │ └── index.ts │ │ └── models │ │ │ ├── ApiClient.interface.ts │ │ │ ├── index.ts │ │ │ ├── items │ │ │ ├── ItemsApiClient.interface.ts │ │ │ ├── ItemsApiClient.model.ts │ │ │ ├── ItemsApiClientOptions.interface.ts │ │ │ └── index.ts │ │ │ └── localization │ │ │ ├── LocalizationApiClient.interface.ts │ │ │ ├── LocalizationApiClient.model.ts │ │ │ ├── LocalizationApiClientOptions.interface.ts │ │ │ └── index.ts │ ├── components │ │ ├── items │ │ │ ├── Header.tsx │ │ │ ├── ItemsList.component.tsx │ │ │ └── children │ │ │ │ ├── Item.behavior.test.tsx │ │ │ │ ├── Item.component.tsx │ │ │ │ └── Item.rendering.test.tsx │ │ ├── primitives │ │ │ ├── buttons │ │ │ │ ├── ButtonCssStrategy.ts │ │ │ │ └── ElButton.tsx │ │ │ ├── icons │ │ │ │ ├── ElIconAlert.tsx │ │ │ │ ├── IconProps.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── modals │ │ │ │ ├── ElModal.ts │ │ │ │ ├── ModalProps.interface.ts │ │ │ │ └── useModal.ts │ │ │ ├── text │ │ │ │ └── ElText.tsx │ │ │ └── toggles │ │ │ │ └── ElToggle.tsx │ │ └── shared │ │ │ ├── DebugFormatters.component.tsx │ │ │ ├── Loader.component.tsx │ │ │ └── LocaleSelector.component.tsx │ ├── config │ │ ├── config-files-map.ts │ │ ├── config-files │ │ │ ├── beta.json │ │ │ ├── jsonserver.json │ │ │ ├── localapis.json │ │ │ ├── mock.json │ │ │ └── production.json │ │ ├── index.ts │ │ ├── models │ │ │ └── Config.interface.ts │ │ └── utils.ts │ ├── favicon.svg │ ├── http-client │ │ ├── index.ts │ │ └── models │ │ │ ├── Constants.ts │ │ │ ├── HttpClient.axios.ts │ │ │ ├── HttpClient.fetch.ts │ │ │ ├── HttpClient.interface.ts │ │ │ ├── HttpRequestParams.interface.ts │ │ │ ├── UrlUtils.ts │ │ │ └── index.ts │ ├── icons │ │ └── Moon.tsx │ ├── localization │ │ ├── i18n.init.ts │ │ ├── index.ts │ │ └── useLocalization.ts │ ├── logo.svg │ ├── main.tsx │ ├── models │ │ ├── index.ts │ │ └── items │ │ │ ├── Item.interface.ts │ │ │ └── index.ts │ ├── store │ │ ├── index.ts │ │ ├── items │ │ │ ├── Items.slice.ts │ │ │ ├── Items.store.ts │ │ │ ├── index.ts │ │ │ └── models │ │ │ │ ├── ItemsState.interface.ts │ │ │ │ └── index.ts │ │ └── root │ │ │ ├── Root.store.ts │ │ │ ├── index.ts │ │ │ └── models │ │ │ ├── RootStore.interface.ts │ │ │ └── index.ts │ ├── tailwind │ │ ├── app.css │ │ └── other.css │ ├── test-utils │ │ └── index.ts │ ├── tests │ │ └── unit │ │ │ ├── config │ │ │ ├── config-files-map.test.ts │ │ │ └── config.mock.test.ts │ │ │ └── http-client │ │ │ ├── UrlUtils.getFullUrlWithParams.test.ts │ │ │ ├── axios-client │ │ │ ├── AxiosClient.request.get.test.ts │ │ │ └── AxiosClient.request.post.test.ts │ │ │ └── fetch-client │ │ │ └── FetchClient.request.get.test.ts │ ├── views │ │ ├── Items.view.tsx │ │ └── Primitives.view.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.jsonserver.ts ├── vite.config.mock.ts └── vite.config.production.ts ├── readme-images └── react-typescript-300.png ├── steps-by-chapter ├── chapter-01 │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── chapter-02 │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ └── items │ │ │ │ ├── ItemsList.component.tsx │ │ │ │ └── ItemsList.with-class.component.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── chapter-03 │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ └── items │ │ │ │ ├── ItemsList.component.tsx │ │ │ │ └── ItemsList.with-class-syntax.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── models │ │ │ └── items │ │ │ │ └── Item.interface.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── chapter-04 │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ └── items │ │ │ │ ├── ItemsList.component.tsx │ │ │ │ └── ItemsList.with-class-syntax.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── models │ │ │ └── items │ │ │ │ └── Item.interface.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── chapter-05 │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ └── items │ │ │ ├── ItemsList.component.tsx │ │ │ ├── ItemsList.with-class-syntax.tsx │ │ │ └── children │ │ │ ├── Item.behavior.test.tsx │ │ │ ├── Item.component.tsx │ │ │ └── Item.rendering.test.tsx │ ├── index.css │ ├── main.tsx │ ├── models │ │ └── items │ │ │ └── Item.interface.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── with-create-react-app ├── .gitignore ├── create-react-app-readme.bak ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static │ └── data │ └── items.json ├── src ├── App.css ├── App.test.bak ├── App.tsx ├── api-client │ ├── index.ts │ ├── live │ │ ├── index.ts │ │ └── items │ │ │ └── index.ts │ └── mock │ │ ├── index.ts │ │ └── items │ │ └── index.ts ├── components │ ├── items │ │ ├── ItemsList.component.tsx │ │ └── children │ │ │ ├── Item.behavior.test.tsx │ │ │ ├── Item.component.tsx │ │ │ └── Item.rendering.test.tsx │ └── shared │ │ └── Loader.component.tsx ├── http-client │ ├── index.ts │ └── models │ │ ├── HttpClient.axios.ts │ │ ├── HttpClient.interface.ts │ │ ├── HttpRequestParams.interface.ts │ │ ├── HttpRequestType.enum.ts │ │ └── index.ts ├── index.css ├── index.tsx ├── logo.svg ├── models │ ├── api-client │ │ ├── ApiClient.interface.ts │ │ └── items │ │ │ ├── ItemsApiClient.interface.ts │ │ │ ├── ItemsApiClient.model.ts │ │ │ ├── ItemsApiClientUrls.interface.ts │ │ │ └── index.ts │ └── items │ │ └── Item.interface.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── shims-react.d.ts ├── store │ ├── index.ts │ ├── items │ │ ├── Items.slice.ts │ │ ├── Items.store.ts │ │ ├── index.ts │ │ └── models │ │ │ ├── ItemsState.interface.ts │ │ │ ├── ItemsStore.interface.ts │ │ │ └── index.ts │ └── root │ │ ├── Root.store.ts │ │ ├── index.ts │ │ └── models │ │ ├── RootStore.interface.ts │ │ └── index.ts ├── tests │ └── http-client │ │ ├── HttpClient.request.get.test.ts │ │ └── HttpClient.request.post.test.ts └── views │ └── Items.view.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | backup/* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/* 2 | coverage/* 3 | node_modules/* 4 | dist/* 5 | public/* 6 | unused-code/* 7 | *.d.ts 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "vueIndentScriptAndStyle": true, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Damiano Fusco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Large Scale Apps with React and TypeScript 2 | 3 | ### Companion code for the book: 4 | 5 | 6 | [Large Scale Apps with React and TypeScript]( 7 | https://leanpub.com/react-typescript "Large Scale Apps with React and TypeScript") 8 | 9 | 10 | ### Running Samples: 11 | # coming soon... 12 | 13 | 14 | ### Screenshot as of Chapter ... 15 | # coming soon... 16 | 17 | 18 | ### Note 19 | The folder my-react-app contains the main project built throughout the book. 20 | -------------------------------------------------------------------------------- /my-component-library/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /my-component-library/README.md: -------------------------------------------------------------------------------- 1 | # my-component-library 2 | 3 | # dependencies 4 | "@types/react": "^18.2.34", 5 | "@types/react-dom": "^18.2.14", 6 | "@vitejs/plugin-react": "^4.1.1", 7 | "react": "^18.2.0", 8 | "react-dom": "^18.2.0", 9 | "typescript": "^5.2.2", 10 | "vite": "^4.5.0" 11 | 12 | ### old dependencies 13 | 14 | "@types/react": "^18.0.25", 15 | "@types/react-dom": "^18.0.9", 16 | "@vitejs/plugin-react": "^2.2.0", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "typescript": "^4.9.3", 20 | "vite": "^3.2.4" -------------------------------------------------------------------------------- /my-component-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-component-library", 3 | "version": "0.1.3", 4 | "type": "module", 5 | "scripts": { 6 | "clean": "rm -rf ./dist; rm -rf my-component-library-0.1.3.tgz; rm -rf ../my-component-library-0.1.3.tgz", 7 | "build-types": "tsc --declaration --emitDeclarationOnly --outDir ./dist", 8 | "build-lib": "vite build", 9 | "build": "npm run clean && npm run build-lib && npm run build-types", 10 | "pack": "npm pack; mv my-component-library-0.1.3.tgz ../my-component-library-0.1.3.tgz", 11 | "all": "npm run build && npm run pack", 12 | "preversion": "npm run clean", 13 | "version": "npm run build", 14 | "postversion": "npm run pack", 15 | "version-patch": "npm version patch -m \"Patch version\"" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "types": "./dist/src/index.d.ts", 21 | "main": "./dist/my-component-lib.umd.js", 22 | "module": "./dist/my-component-lib.es.js", 23 | "exports": { 24 | ".": { 25 | "import": [ 26 | "./dist/my-component-lib.es.js" 27 | ], 28 | "require": "./dist/my-component-lib.umd.js" 29 | }, 30 | "./package.json": "./package.json" 31 | }, 32 | "devDependencies": { 33 | "@types/react": "^18.2.34", 34 | "@types/react-dom": "^18.2.14", 35 | "@vitejs/plugin-react": "^4.1.1", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "typescript": "^5.2.2", 39 | "vite": "^4.5.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /my-component-library/src/components/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/counter/Counter.tsx 2 | import * as React from 'react' 3 | 4 | export function Counter() { 5 | let [count, setCount] = React.useState(0) 6 | 7 | const increment = () => { 8 | setCount(count + 1) 9 | } 10 | 11 | return ( 12 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /my-component-library/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { SampleComp } from './sample-component/SampleComp' 2 | import { Counter } from './counter/Counter' 3 | 4 | export { 5 | SampleComp, 6 | Counter 7 | } 8 | -------------------------------------------------------------------------------- /my-component-library/src/components/sample-component/SampleComp.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/counter/SampleComp.tsx 2 | import * as React from 'react' 3 | 4 | type Props = { 5 | testid?: string 6 | text?: string 7 | } 8 | 9 | export function SampleComp(props: Props) { 10 | const testid = props.testid || 'not-set' 11 | const text = props.text || 'not-set' 12 | 13 | // a computed property to return the css class 14 | const cssClass = () => { 15 | return `p-2 border border-green-500` 16 | } 17 | 18 | return ( 19 |
20 | { text } 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /my-component-library/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | -------------------------------------------------------------------------------- /my-component-library/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /my-component-library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "react-jsx" 17 | }, 18 | "include": ["src"], 19 | "references": [{ "path": "./tsconfig.node.json" }] 20 | } 21 | -------------------------------------------------------------------------------- /my-component-library/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /my-component-library/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | envDir: './src/', 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, 'src/') 12 | }, 13 | }, 14 | build: { 15 | lib: { 16 | entry: path.resolve(__dirname, 'src/index.ts'), 17 | name: "MyComponentLib", 18 | fileName: (format) => `my-component-lib.${format}.js`, 19 | }, 20 | rollupOptions: { 21 | // React should not be bundled with the cmoponent library 22 | // tell vite that this is an external dependency 23 | external: ['react'], 24 | output: { 25 | // To expose global variables for use in the UMD builds 26 | // for external dependencies 27 | globals: { 28 | vue: 'React' 29 | } 30 | } 31 | } 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /my-js-helpers/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /my-js-helpers/.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/* 2 | coverage/* 3 | node_modules/* 4 | public/* 5 | dist/* 6 | unused-code/* 7 | *.d.ts 8 | -------------------------------------------------------------------------------- /my-js-helpers/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "vueIndentScriptAndStyle": true, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /my-js-helpers/README.md: -------------------------------------------------------------------------------- 1 | # @largescaleapps/my-js-helpers 2 | 3 | Ignore this NPM package. It is just a sample project with sample code for a book. 4 | -------------------------------------------------------------------------------- /my-js-helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@largescaleapps/my-js-helpers", 3 | "version": "0.1.5", 4 | "type": "module", 5 | "scripts": { 6 | "clean": "rm -rf ./dist; rm -rf my-js-helpers-0.1.5.tgz; rm -rf ../my-js-helpers-0.1.5.tgz", 7 | "build-types": "tsc --declaration --emitDeclarationOnly --outDir ./dist", 8 | "build-lib": "vite build", 9 | "build": "npm run clean && npm run build-lib && npm run build-types", 10 | "pack": "npm pack; mv my-js-helpers-0.1.5.tgz ../my-js-helpers-0.1.5.tgz", 11 | "all": "npm run build && npm run pack", 12 | "pub": "npm publish --access public", 13 | "test": "vitest run", 14 | "test-watch": "vitest watch", 15 | "pretty": "prettier -w \"./src/**/*.ts\"", 16 | "preversion": "npm run clean", 17 | "version": "npm run build", 18 | "postversion": "npm run pack", 19 | "version-patch": "npm version patch -m \"Patch version\"" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^29.5.7", 23 | "jsdom": "^22.1.0", 24 | "typescript": "^5.2.2", 25 | "vite": "^4.5.0", 26 | "vitest": "^0.34.6" 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "types": "./dist/src/index.d.ts", 32 | "main": "./dist/my-js-helpers.umd.js", 33 | "module": "./dist/my-js-helpers.es.js", 34 | "exports": { 35 | ".": { 36 | "import": [ 37 | "./dist/my-js-helpers.es.js" 38 | ], 39 | "require": "./dist/my-js-helpers.umd.js" 40 | }, 41 | "./package.json": "./package.json" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /my-js-helpers/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { randomid } from './random-id/random-id' 2 | 3 | export { randomid } 4 | -------------------------------------------------------------------------------- /my-js-helpers/src/helpers/random-id/random-id.ts: -------------------------------------------------------------------------------- 1 | export const randomid = (): string => { 2 | let result: string = '' 3 | if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { 4 | const array: Uint32Array = new Uint32Array(1) 5 | window.crypto.getRandomValues(array) 6 | result = array[0].toString() 7 | } else { 8 | // throw error 9 | // throw Error('Browser does not support window.crypto.getRandomValues') 10 | // if node, we could use crypto to do the same thing 11 | result = require('crypto').randomBytes(5).toString('hex') 12 | } 13 | 14 | // pad the result with zero to make sure is always the same length (11 chars in our case) 15 | if (result.length < 11) { 16 | result = result.padStart(11, '0') 17 | } 18 | 19 | return result 20 | } 21 | -------------------------------------------------------------------------------- /my-js-helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers' 2 | -------------------------------------------------------------------------------- /my-js-helpers/src/tests/random-id/randomid.test.ts: -------------------------------------------------------------------------------- 1 | import { randomid } from '../../helpers' 2 | 3 | describe('id', () => { 4 | it('should return value with expected length', () => { 5 | const result = randomid() 6 | expect(result.length).toEqual(11) 7 | }) 8 | 9 | it('should return expected value', () => { 10 | // testing 10,000 different ids 11 | const attempts = 10000 12 | const results = [] 13 | for (let i = 0; i < attempts; i++) { 14 | const value = randomid() 15 | results.push(value) 16 | } 17 | 18 | const distinctResults = new Set([...results]) 19 | expect(results.length).toEqual(distinctResults.size) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /my-js-helpers/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /my-js-helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "noEmit": false, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | } 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /my-js-helpers/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite' 5 | import path from 'path' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | ], 11 | envDir: './src/', 12 | resolve: { 13 | alias: { 14 | '@': path.resolve(__dirname, 'src/') 15 | }, 16 | }, 17 | test: { 18 | globals: true, 19 | environment: 'jsdom', 20 | exclude: [ 21 | 'node_modules' 22 | ] 23 | }, 24 | build: { 25 | lib: { 26 | entry: path.resolve(__dirname, 'src/index.ts'), 27 | name: 'MyJsHelpers', 28 | fileName: (format) => `my-js-helpers.${format}.js`, 29 | }, 30 | rollupOptions: { 31 | external: [], 32 | output: { 33 | // Provide global variables to use in the UMD build 34 | // Add external deps here 35 | globals: { 36 | }, 37 | }, 38 | }, 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /my-react-project/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .vscode -------------------------------------------------------------------------------- /my-react-project/.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/* 2 | coverage/* 3 | node_modules/* 4 | dist/* 5 | public/* 6 | unused-code/* 7 | *.d.ts 8 | -------------------------------------------------------------------------------- /my-react-project/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120, 7 | "vueIndentScriptAndStyle": true, 8 | "trailingComma": "none" 9 | } 10 | -------------------------------------------------------------------------------- /my-react-project/NOTES.md: -------------------------------------------------------------------------------- 1 | npm uninstall --save jest @testing-library/jest-dom @types/jest ts-jest @types/testing-library__jest-dom 2 | 3 | npm install --save-dev vitest c8 jsdom @testing-library/user-event 4 | 5 | package.json: 6 | "test": "vitest run", 7 | "test-watch": "vitest wach", 8 | 9 | tsconfig.json (compilerOptions): 10 | "types": [ 11 | "react", 12 | "vite/client", 13 | "vitest/globals" 14 | ], 15 | "skipLibCheck": true, 16 | 17 | 18 | vite.config.js: 19 | /// 20 | /// 21 | 22 | # within defineComponent: 23 | , 24 | test: { 25 | globals: true, 26 | environment: 'jsdom', 27 | exclude: [ 28 | 'node_modules' 29 | ] 30 | } 31 | 32 | 33 | src/test-utils/index.tsx 34 | -------------------------------------------------------------------------------- /my-react-project/README.md: -------------------------------------------------------------------------------- 1 | # my-react-project 2 | 3 | Companion code for the book "Large Scale Apps with React and TypeScript": 4 | 5 | [https://www.damianofusco.com/](https://www.damianofusco.com/) 6 | -------------------------------------------------------------------------------- /my-react-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My React Project 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /my-react-project/json-server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "items": [ 4 | { 5 | "id": 1, 6 | "name": "Item 1 from json-server", 7 | "selected": false 8 | }, 9 | { 10 | "id": 2, 11 | "name": "Item 2 from json-server", 12 | "selected": false 13 | }, 14 | { 15 | "id": 3, 16 | "name": "Item 3 from json-server", 17 | "selected": false 18 | }, 19 | { 20 | "id": 4, 21 | "name": "Item 4 from json-server", 22 | "selected": false 23 | }, 24 | { 25 | "id": 5, 26 | "name": "Item 5 from json-server", 27 | "selected": false 28 | } 29 | ], 30 | 31 | "translation_en-US": { 32 | "locale.selector.en-US": "English", 33 | "locale.selector.it-IT": "Italian", 34 | "locale.selector.fr-FR": "French", 35 | "locale.selector.es-ES": "Spanish", 36 | "home.welcome": "Welcome: this message is localized in English", 37 | "navigation.home": "Home", 38 | "navigation.about": "About", 39 | "items.list.header": "My Items" 40 | }, 41 | 42 | "translation_it-IT": { 43 | "locale.selector.en-US": "Inglese", 44 | "locale.selector.it-IT": "Italiano", 45 | "locale.selector.fr-FR": "Francese", 46 | "locale.selector.es-ES": "Spagnolo", 47 | 48 | "home.welcome": "Benvenuti: this message is localized in Italian", 49 | 50 | "navigation.home": "Home", 51 | "navigation.about": "Chi Siamo", 52 | 53 | "items.list.header": "I miei articoli" 54 | }, 55 | 56 | "translation_fr-FR": { 57 | "locale.selector.en-US": "Anglais", 58 | "locale.selector.it-IT": "Italien", 59 | "locale.selector.fr-FR": "Français", 60 | "locale.selector.es-ES": "Espagnol", 61 | 62 | "home.welcome": "Bienvenue: this message is localized in French", 63 | 64 | "navigation.home": "Accueil", 65 | "navigation.about": "À propos de nous", 66 | 67 | "items.list.header": "Mes articles" 68 | }, 69 | 70 | "translation_es-ES": { 71 | "locale.selector.en-US": "Inglés", 72 | "locale.selector.it-IT": "Italiano", 73 | "locale.selector.fr-FR": "Francés", 74 | "locale.selector.es-ES": "Español", 75 | 76 | "home.welcome": "Bienvenido: this message is localized in Spanish", 77 | 78 | "navigation.home": "Inicio", 79 | "navigation.about": "Acerca de", 80 | 81 | "items.list.header": "Mis cosas" 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /my-react-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react-project", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite --config vite.config.mock.ts --mode mock", 6 | "build": "tsc && vite build --config vite.config.production.ts --mode production", 7 | "build-beta": "tsc && vite build --config vite.config.production.ts --mode beta", 8 | "build-local": "tsc && vite build --config vite.config.production.ts --mode localapis", 9 | "build-mock": "tsc && vite build --config vite.config.mock.ts --mode mock", 10 | "preview": "vite preview --config vite.config.mock.ts --mode mock", 11 | "start": "npm run dev", 12 | "start-local": "vite --config vite.config.production.ts --mode localapis", 13 | "with-jsonserver": "vite --config vite.config.jsonserver.ts --mode jsonserver", 14 | "json-server-api": "json-server --port 3111 --watch json-server/db.json", 15 | "test": "vitest run --config vite.config.mock.ts --mode mock", 16 | "test-watch": "vitest watch --config vite.config.mock.ts --mode mock", 17 | "test-coverage": "vitest run --coverage --config vite.config.mock.ts --mode mock", 18 | "pretty": "prettier -w \"./src/**/*.*{ts,tsx,vue,svelte,css,scss,html}\"", 19 | "check": "npm run pretty; npm run test; npm run build-mock" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "pretty-quick --staged \"./src/**/*.*{ts,tsx,vue,svelte,css,scss,html}\"" 24 | } 25 | }, 26 | "devDependencies": { 27 | "@builtwithjavascript/formatters": "^1.0.8", 28 | "@largescaleapps/my-js-helpers": "^0.1.5", 29 | "@reduxjs/toolkit": "^1.9.7", 30 | "@testing-library/react": "^14.0.0", 31 | "@testing-library/user-event": "^14.5.1", 32 | "@types/node": "^20.14.10", 33 | "@types/react": "^18.2.34", 34 | "@types/react-dom": "^18.2.14", 35 | "@vitejs/plugin-react": "^4.3.1", 36 | "autoprefixer": "^10.4.19", 37 | "axios": "^1.6.0", 38 | "c8": "^10.1.2", 39 | "husky": "^8.0.3", 40 | "i18next": "^23.6.0", 41 | "i18next-http-backend": "^2.3.1", 42 | "jsdom": "^24.1.0", 43 | "json-server": "^1.0.0-beta.1", 44 | "my-component-library": "file:../my-component-library", 45 | "postcss": "^8.4.39", 46 | "react": "^18.2.0", 47 | "react-dom": "^18.2.0", 48 | "react-i18next": "^13.3.1", 49 | "react-redux": "^8.1.3", 50 | "tailwindcss": "^3.4.5", 51 | "typescript": "^5.5.3", 52 | "vite": "^5.3.3", 53 | "vitest": "^2.0.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /my-react-project/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /my-react-project/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/my-react-project/public/favicon.ico -------------------------------------------------------------------------------- /my-react-project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /my-react-project/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/my-react-project/public/logo192.png -------------------------------------------------------------------------------- /my-react-project/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/my-react-project/public/logo512.png -------------------------------------------------------------------------------- /my-react-project/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 | -------------------------------------------------------------------------------- /my-react-project/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /my-react-project/public/static/mock-data/items/items.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "Item 1", 4 | "selected": false 5 | }, { 6 | "id": 2, 7 | "name": "Item 2", 8 | "selected": false 9 | }, { 10 | "id": 3, 11 | "name": "Item 3", 12 | "selected": false 13 | }, { 14 | "id": 4, 15 | "name": "Item 4", 16 | "selected": false 17 | }, { 18 | "id": 5, 19 | "name": "Item 5", 20 | "selected": false 21 | }] 22 | -------------------------------------------------------------------------------- /my-react-project/public/static/mock-data/localization/translation/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale.selector.en-US": "English", 3 | "locale.selector.it-IT": "Italian", 4 | "locale.selector.fr-FR": "French", 5 | "locale.selector.es-ES": "Spanish", 6 | 7 | "home.welcome": "Welcome: this message is localized in English", 8 | 9 | "navigation.home": "Home", 10 | "navigation.about": "About", 11 | 12 | "items.list.header": "My Items" 13 | } 14 | -------------------------------------------------------------------------------- /my-react-project/public/static/mock-data/localization/translation/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale.selector.en-US": "Inglés", 3 | "locale.selector.it-IT": "Italiano", 4 | "locale.selector.fr-FR": "Francés", 5 | "locale.selector.es-ES": "Español", 6 | 7 | "home.welcome": "Bienvenido: this message is localized in Spanish", 8 | 9 | "navigation.home": "Inicio", 10 | "navigation.about": "Acerca de", 11 | 12 | "items.list.header": "Mis cosas" 13 | } 14 | -------------------------------------------------------------------------------- /my-react-project/public/static/mock-data/localization/translation/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale.selector.en-US": "Anglais", 3 | "locale.selector.it-IT": "Italien", 4 | "locale.selector.fr-FR": "Français", 5 | "locale.selector.es-ES": "Espagnol", 6 | 7 | "home.welcome": "Bienvenue: this message is localized in French", 8 | 9 | "navigation.home": "Accueil", 10 | "navigation.about": "À propos de nous", 11 | 12 | "items.list.header": "Mes articles" 13 | } 14 | -------------------------------------------------------------------------------- /my-react-project/public/static/mock-data/localization/translation/it-IT.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale.selector.en-US": "Inglese", 3 | "locale.selector.it-IT": "Italiano", 4 | "locale.selector.fr-FR": "Francese", 5 | "locale.selector.es-ES": "Spagnolo", 6 | 7 | "home.welcome": "Benvenuti: this message is localized in Italian", 8 | 9 | "navigation.home": "Home", 10 | "navigation.about": "Chi Siamo", 11 | 12 | "items.list.header": "I miei articoli" 13 | } 14 | -------------------------------------------------------------------------------- /my-react-project/src/.env.beta: -------------------------------------------------------------------------------- 1 | VITE_APP_CONFIG=beta 2 | -------------------------------------------------------------------------------- /my-react-project/src/.env.jsonserver: -------------------------------------------------------------------------------- 1 | VITE_APP_CONFIG=jsonserver 2 | -------------------------------------------------------------------------------- /my-react-project/src/.env.localapis: -------------------------------------------------------------------------------- 1 | VITE_APP_CONFIG=localapis 2 | -------------------------------------------------------------------------------- /my-react-project/src/.env.mock: -------------------------------------------------------------------------------- 1 | VITE_APP_CONFIG=mock 2 | -------------------------------------------------------------------------------- /my-react-project/src/.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_CONFIG=production 2 | -------------------------------------------------------------------------------- /my-react-project/src/App.tsx: -------------------------------------------------------------------------------- 1 | // file: src/App.tsx 2 | import * as React from 'react' 3 | 4 | // import a reference to Redux Provider and our rootStore 5 | import { Provider } from 'react-redux' 6 | import { rootStore } from '@/store' 7 | // import a reference to useLocalization 8 | import { useLocalization } from '@/localization' 9 | 10 | // import a reference to the Items View 11 | import ItemsView from '@/views/Items.view' 12 | import PrimitivesView from '@/views/Primitives.view' 13 | 14 | import { LocaleSelector } from '@/components/shared/LocaleSelector.component' 15 | import { DebugFormatters } from '@/components/shared/DebugFormatters.component' 16 | 17 | // App component: 18 | function App() { 19 | // get what we need from useLocalization: 20 | const { t, locales, currentLocale, changeLocale } = useLocalization() 21 | 22 | // an event handler from cahnging the locale 23 | const onLocaleClick = (lcid: string) => { 24 | changeLocale(lcid) 25 | } 26 | 27 | return ( 28 | 29 | {/* wrap the root App element with Redux store provider */} 30 |
31 | 32 |

{t('home.welcome')}

{/* update this to use the t function to translate our welcome message */} 33 | 34 | 35 | 36 |
37 |
38 | ) 39 | } 40 | 41 | export default App 42 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/index.ts 2 | 3 | import { ApiClientInterface } from './models' 4 | import { apiMockClient } from './mock' 5 | import { apiLiveClient } from './live' 6 | 7 | import { config } from '../config' 8 | 9 | // return either the live or the mock client 10 | let apiClient: ApiClientInterface 11 | if (config.apiClient.type === 'live') { 12 | apiClient = apiLiveClient 13 | } else { 14 | // default is always apiMockClient 15 | apiClient = apiMockClient 16 | } 17 | 18 | export { apiClient } 19 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/live/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/live/index.ts 2 | 3 | import { ApiClientInterface } from '../models' 4 | // import module instances 5 | import { localizationApiClient } from './localization' 6 | import { itemsApiClient } from './items' 7 | 8 | // create an instance of our main ApiClient that wraps the live child clients 9 | const apiLiveClient: ApiClientInterface = { 10 | localization: localizationApiClient, 11 | items: itemsApiClient 12 | } 13 | // export our instance 14 | export { apiLiveClient } 15 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/live/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/live/items/index.ts 2 | 3 | // import a reference to the app config 4 | import { config } from '@/config' 5 | 6 | import { ItemsApiClientInterface, ItemsApiClientModel } from '../../models' 7 | 8 | // instantiate the ItemsApiClient pointing at the url that returns live data 9 | const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(config.items.apiClientOptions) 10 | 11 | // export our instance 12 | export { itemsApiClient } 13 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/live/localization/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/live/localization/index.ts 2 | 3 | // import a reference to the app config 4 | import { config } from '@/config' 5 | 6 | import { LocalizationApiClientInterface, LocalizationApiClientModel } from '../../models' 7 | 8 | // instantiate the LocalizationApiClient pointing at the url that returns static json mock data 9 | const localizationApiClient: LocalizationApiClientInterface = new LocalizationApiClientModel( 10 | config.localization.apiClientOptions 11 | ) 12 | 13 | // export our instance 14 | export { localizationApiClient } 15 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/mock/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/mock/index.ts 2 | 3 | import { ApiClientInterface } from '../models' 4 | // import module instances 5 | import { localizationApiClient } from './localization' 6 | import { itemsApiClient } from './items' 7 | 8 | // create an instance of our main ApiClient that wraps the mock child clients 9 | const apiMockClient: ApiClientInterface = { 10 | localization: localizationApiClient, 11 | items: itemsApiClient 12 | } 13 | 14 | // export our instance 15 | export { apiMockClient } 16 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/mock/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/mock/items/index.ts 2 | 3 | // import a reference to the app config 4 | import { config } from '@/config' 5 | 6 | import { ItemsApiClientInterface, ItemsApiClientModel } from '../../models' 7 | 8 | // instantiate the ItemsApiClient pointing at the url that returns static json mock data 9 | const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(config.items.apiClientOptions) 10 | 11 | // export our instance 12 | export { itemsApiClient } 13 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/mock/localization/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/mock/localization/index.ts 2 | 3 | // import a reference to the app config 4 | import { config } from '@/config' 5 | 6 | import { LocalizationApiClientInterface, LocalizationApiClientModel } from '../../models' 7 | 8 | // instantiate the LocalizationApiClient pointing at the url that returns static json mock data 9 | const localizationApiClient: LocalizationApiClientInterface = new LocalizationApiClientModel( 10 | config.localization.apiClientOptions 11 | ) 12 | 13 | // export our instance 14 | export { localizationApiClient } 15 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/ApiClient.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/ApiClient.interface.ts 2 | 3 | import { LocalizationApiClientInterface } from './localization' 4 | import { ItemsApiClientInterface } from './items' 5 | 6 | /** 7 | * @Name ApiClientInterface 8 | * @description 9 | * Interface wraps all api client modules into one places for keeping code organized. 10 | */ 11 | export interface ApiClientInterface { 12 | localization: LocalizationApiClientInterface 13 | items: ItemsApiClientInterface 14 | } 15 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/index.ts 2 | 3 | export * from './ApiClient.interface' 4 | export * from './items' 5 | export * from './localization' 6 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/items/ItemsApiClient.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/items/ItemsApiClient.interface.ts 2 | 3 | import { ItemInterface } from '../../../models/items/Item.interface' 4 | 5 | /** 6 | * @Name ItemsApiClientInterface 7 | * @description 8 | * Interface for the Items api client module 9 | */ 10 | export interface ItemsApiClientInterface { 11 | fetchItems: () => Promise 12 | } 13 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/items/ItemsApiClient.model.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/items/ItemsApiClient.model.ts 2 | 3 | import { useHttpClient, HttpRequestParamsInterface, HttpRequestType } from '@/http-client' 4 | 5 | import { ItemsApiClientOptions, ItemsApiClientEndpoints } from './ItemsApiClientOptions.interface' 6 | import { ItemsApiClientInterface } from './ItemsApiClient.interface' 7 | import { ItemInterface } from '@/models' 8 | 9 | /** 10 | * @Name ItemsApiClientModel 11 | * @description 12 | * Implements the ItemsApiClientInterface interface 13 | */ 14 | export class ItemsApiClientModel implements ItemsApiClientInterface { 15 | private readonly endpoints!: ItemsApiClientEndpoints 16 | private readonly mockDelay: number = 0 17 | 18 | constructor(options: ItemsApiClientOptions) { 19 | this.endpoints = options.endpoints 20 | if (options.mockDelay) { 21 | this.mockDelay = options.mockDelay 22 | } 23 | } 24 | 25 | fetchItems(): Promise { 26 | const requestParameters: HttpRequestParamsInterface = { 27 | requestType: HttpRequestType.get, 28 | endpoint: this.endpoints.fetchItems, 29 | requiresToken: false, 30 | mockDelay: this.mockDelay 31 | } 32 | 33 | return useHttpClient().request(requestParameters) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/items/ItemsApiClientOptions.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/items/ItemsApiClientOptions.interface.ts 2 | 3 | /** 4 | * @Name ItemsApiClientEndpoints 5 | * @description 6 | * Interface for the Items urls used to avoid hard-coded strings 7 | */ 8 | export interface ItemsApiClientEndpoints { 9 | fetchItems: string 10 | } 11 | 12 | /** 13 | * @Name ItemsApiClientOptions 14 | * @description 15 | * Interface for the Items api client options (includes endpoints used to avoid hard-coded strings) 16 | */ 17 | export interface ItemsApiClientOptions { 18 | mockDelay?: number 19 | endpoints: ItemsApiClientEndpoints 20 | } 21 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/items/index.ts 2 | 3 | export * from './ItemsApiClientOptions.interface' 4 | export * from './ItemsApiClient.interface' 5 | export * from './ItemsApiClient.model' 6 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/localization/LocalizationApiClient.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/localization/LocalizationApiClient.interface.ts 2 | 3 | /** 4 | * @Name LocalizationApiClientInterface 5 | * @description 6 | * Interface for the Localization api client module 7 | */ 8 | export interface LocalizationApiClientInterface { 9 | fetchTranslation: (namespace: string, key: string) => Promise<{ [key: string]: string }> 10 | } 11 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/localization/LocalizationApiClient.model.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/localization/LocalizationApiClient.model.ts 2 | 3 | import { useHttpClient, HttpRequestParamsInterface, HttpRequestType } from '@/http-client' 4 | 5 | import { LocalizationApiClientOptions, LocalizationApiClientEndpoints } from './LocalizationApiClientOptions.interface' 6 | import { LocalizationApiClientInterface } from './LocalizationApiClient.interface' 7 | 8 | /** 9 | * @Name LocalizationApiClientModel 10 | * @description 11 | * Implements the LocalizationApiClientInterface interface 12 | */ 13 | export class LocalizationApiClientModel implements LocalizationApiClientInterface { 14 | private readonly endpoints!: LocalizationApiClientEndpoints 15 | private readonly mockDelay: number = 0 16 | 17 | constructor(options: LocalizationApiClientOptions) { 18 | this.endpoints = options.endpoints 19 | if (options.mockDelay) { 20 | this.mockDelay = options.mockDelay 21 | } 22 | } 23 | 24 | fetchTranslation(namespace: string, key: string): Promise<{ [key: string]: string }> { 25 | const requestParameters: HttpRequestParamsInterface = { 26 | requestType: HttpRequestType.get, 27 | endpoint: this.endpoints.fetchTranslation, 28 | requiresToken: false, 29 | payload: { 30 | namespace, 31 | key 32 | } as any, 33 | mockDelay: this.mockDelay 34 | } 35 | 36 | return useHttpClient().request<{ [key: string]: string }>(requestParameters) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/localization/LocalizationApiClientOptions.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/localization/LocalizationApiClientOptions.interface.ts 2 | 3 | export interface LocalizationApiClientEndpoints { 4 | fetchTranslation: string 5 | } 6 | 7 | /** 8 | * @Name LocalizationApiClientOptions 9 | * @description 10 | * Interface for the Localization api client options (includes endpoints used to avoid hard-coded strings) 11 | */ 12 | export interface LocalizationApiClientOptions { 13 | mockDelay?: number 14 | endpoints: LocalizationApiClientEndpoints 15 | } 16 | -------------------------------------------------------------------------------- /my-react-project/src/api-client/models/localization/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/models/localization/index.ts 2 | 3 | export * from './LocalizationApiClientOptions.interface' 4 | export * from './LocalizationApiClient.interface' 5 | export * from './LocalizationApiClient.model' 6 | -------------------------------------------------------------------------------- /my-react-project/src/components/items/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useLocalization } from '@/localization' 3 | 4 | export function Header() { 5 | const { t } = useLocalization() 6 | 7 | return

{t('items.list.header')}:

8 | } 9 | -------------------------------------------------------------------------------- /my-react-project/src/components/items/ItemsList.component.tsx: -------------------------------------------------------------------------------- 1 | // file: ItemsList.component.tsx 2 | 3 | import React from 'react' 4 | // import reference to our interface 5 | import { ItemInterface } from '@/models/items/Item.interface' 6 | // import reference to your Item component: 7 | import { ItemComponent } from './children/Item.component' 8 | // import a reference to our Loader component: 9 | import { Loader } from '@/components/shared/Loader.component' 10 | 11 | import { Header } from './Header' 12 | 13 | type Props = { 14 | loading: boolean 15 | items: ItemInterface[] 16 | onItemSelect: (item: ItemInterface) => void 17 | } 18 | 19 | // ItemsList component 20 | export class ItemsListComponent extends React.Component { 21 | constructor(props: Props) { 22 | super(props) 23 | } 24 | 25 | handleItemClick(item: ItemInterface) { 26 | this.props.onItemSelect(item) 27 | } 28 | 29 | render(): React.ReactNode { 30 | const { loading, items } = this.props 31 | 32 | let element 33 | if (loading) { 34 | // render Loader 35 | element = 36 | } else { 37 | // render
    38 | element = ( 39 |
      40 | {items.map((item, index) => { 41 | return ( 42 | this.handleItemClick(item)} 48 | > 49 | ) 50 | })} 51 |
    52 | ) 53 | } 54 | 55 | return ( 56 |
    57 |
    58 | {element} 59 |
    60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /my-react-project/src/components/items/children/Item.behavior.test.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/children/Item.behavior.test.tsx 2 | // directive to instruct vitest to use the jsdom environment: 3 | // @vitest-environment jsdom 4 | // import references to what we need from our test-utils: 5 | import { render, fireEvent } from '@/test-utils' 6 | 7 | // import reference to our interface 8 | import { ItemInterface } from '@/models/items/Item.interface' 9 | // import reference to your Item component: 10 | import { ItemComponent } from './Item.component' 11 | 12 | describe('Item.component: behavior', () => { 13 | // test our component click event 14 | it('click event invokes onItemSelect handler as expected', () => { 15 | const model: ItemInterface = { 16 | id: 1, 17 | name: 'Unit test item 1', 18 | selected: false 19 | } 20 | 21 | // create a spy function with vitest.fn() 22 | const onItemSelect = vitest.fn() 23 | 24 | // render our component 25 | const { container } = render() 26 | // get a reference to the
  • element 27 | const liElement = container.firstChild as HTMLElement 28 | // fire click 29 | fireEvent.click(liElement) 30 | // check test result 31 | expect(onItemSelect).toHaveBeenCalledTimes(1) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /my-react-project/src/components/items/children/Item.component.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/children/Item.component.tsx 2 | 3 | import * as React from 'react' 4 | // import reference to our interface 5 | import { ItemInterface } from '@/models/items/Item.interface' 6 | // add the following two lines: 7 | import { ElText } from '@/components/primitives/text/ElText' 8 | import { ElToggle } from '@/components/primitives/toggles/ElToggle' 9 | 10 | // example using class syntax 11 | export class ItemComponent extends React.Component<{ 12 | testid?: string 13 | model: ItemInterface 14 | isLast?: boolean 15 | onItemSelect: (item: ItemInterface) => void 16 | }> { 17 | constructor(props: { testid?: string; model: ItemInterface; onItemSelect: (item: ItemInterface) => void }) { 18 | super(props) 19 | } 20 | 21 | get cssClass() { 22 | let css = 'tem flex items-center justify-between cursor-pointer border border-l-4 list-none rounded-sm px-3 py-3' 23 | if (this.props.model?.selected) { 24 | css += ' font-bold bg-pink-200 hover:bg-pink-100 selected' 25 | } else { 26 | css += ' text-gray-500 hover:bg-gray-100' 27 | } 28 | if (!this.props.isLast) { 29 | css += ' border-b-0' 30 | } 31 | return css.trim() 32 | } 33 | 34 | handleItemClick(item: ItemInterface) { 35 | this.props.onItemSelect(item) 36 | } 37 | 38 | //
    {model.name} [{String(model.selected)}]
    39 | 40 | render(): React.ReactNode { 41 | const { model, testid } = this.props 42 | 43 | return ( 44 |
  • this.handleItemClick(model)}> 45 | 46 | 47 |
  • 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/buttons/ButtonCssStrategy.ts: -------------------------------------------------------------------------------- 1 | const buttonCssStrategy = new Map() 2 | 3 | buttonCssStrategy.set('primary', 'bg-blue-500 text-white hover:bg-blue-400 focus:ring-blue-300') 4 | buttonCssStrategy.set('secondary', 'bg-white text-gray-700 border-gray-300 hover:border-gray-700 focus:ring-gray-300') 5 | buttonCssStrategy.set('danger', 'bg-red-500 text-white hover:bg-red-700 focus:ring-red-500') 6 | 7 | export { buttonCssStrategy } 8 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/buttons/ElButton.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/buttons/ElButton.tsx 2 | import { buttonCssStrategy } from './ButtonCssStrategy' 3 | 4 | type ElButtonProps = { 5 | testid?: string 6 | id: string 7 | label: string 8 | disabled?: boolean 9 | addCss?: string 10 | buttonType?: string 11 | onClicked: Function 12 | } 13 | 14 | export function ElButton(props: ElButtonProps) { 15 | const { id, label, onClicked } = props 16 | 17 | const testid = props.testid || 'testid-not-set' 18 | const disabled = props.disabled || false 19 | const buttonType = props.buttonType || 'primary' 20 | const addCss = (props.addCss || '').trim() 21 | 22 | // a computed property to return a different css class based on the selected value 23 | const cssClass = (): string => { 24 | const result = [ 25 | 'font-bold py-1 px-2 inline-flex justify-center rounded-md border shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2' 26 | ] 27 | if (disabled) { 28 | // these are the button CSS classes when disabled 29 | result.push('bg-gray-500 text-gray-300 opacity-50 cursor-not-allowed') 30 | } else { 31 | // these are the button CSS classes when enabled 32 | result.push(buttonCssStrategy.get(buttonType) as string) 33 | } 34 | 35 | // addCss will have additional CSS classes 36 | // we want to apply from where we consume this component 37 | if (addCss.length > 0) { 38 | result.push(addCss) 39 | } 40 | return result.join(' ').trim() 41 | } 42 | 43 | // click handler 44 | const handleClick = () => { 45 | // proceed only if the button is not disabled, otherwise ignore the click 46 | if (!disabled) { 47 | // dispatch a 'clicked' even through Svelte dispatch 48 | onClicked(id) 49 | } 50 | } 51 | 52 | return ( 53 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/icons/ElIconAlert.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/icons/ElIcon.tsx 2 | import * as React from 'react' 3 | import { IconProps } from './IconProps' 4 | 5 | export function ElIconAlert(props: IconProps) { 6 | const testid = props.testid || 'testid-not-set' 7 | const addCss = (props.addCss || '').trim() 8 | 9 | // a computed property the returns the css class value of this component root element 10 | const cssClass = (): string => { 11 | const result = ['h-6 w-6 '] 12 | if ((addCss || '').trim().length > 0) { 13 | result.push(addCss) 14 | } 15 | return result.join(' ').trim() 16 | } 17 | 18 | return ( 19 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/icons/IconProps.ts: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/icons/IconProps.ts 2 | 3 | export interface IconProps { 4 | testid?: string 5 | addCss?: string 6 | } 7 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/icons/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/icons/index.ts 2 | export * from './IconProps' 3 | export * from './ElIconAlert' 4 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/index.ts: -------------------------------------------------------------------------------- 1 | // file src/components/primitives/index.ts 2 | // text 3 | import { ElText } from './text/ElText' 4 | 5 | // buttons 6 | import { ElButton } from './buttons/ElButton' 7 | 8 | // toggles 9 | import { ElToggle } from './toggles/ElToggle' 10 | 11 | export { 12 | // text 13 | ElText, 14 | // buttons 15 | ElButton, 16 | // toggles 17 | ElToggle 18 | } 19 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/modals/ModalProps.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/modals/ModalProps.interface.ts 2 | import { FunctionComponent, ComponentClass } from 'react' 3 | 4 | /** 5 | * @name ModalProps 6 | * @desrciption Interface that represents the public properties of the Modal component 7 | */ 8 | export interface ModalProps { 9 | testid?: string 10 | cancelLabel: string 11 | confirmLabel: string 12 | title?: string 13 | longDesc?: string // optional 14 | primaryButtonType?: string // optional, defaults to 'primary' 15 | icon?: string | FunctionComponent<{ addCss: string }> | ComponentClass<{ addCss: string }, any> 16 | iconAddCss?: string 17 | } 18 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/modals/useModal.ts: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/modals/useModal.ts 2 | import * as React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { ElModal } from './ElModal' 5 | import { ModalProps } from './ModalProps.interface' 6 | 7 | let instance!: any //ElModal 8 | const domTargetId = 'modal' 9 | 10 | /** 11 | * @name useModal 12 | * @param props The modal props 13 | * @returns the Modal component instance 14 | */ 15 | export const useModal = (props: ModalProps) => { 16 | if (!instance) { 17 | // get the modal target dom element by id 18 | let domTarget = document.getElementById(domTargetId) 19 | // if not existing yet, create it with vanilla JS 20 | if (!domTarget) { 21 | domTarget = document.createElement('div') 22 | domTarget.setAttribute('id', domTargetId) 23 | document.body.appendChild(domTarget) 24 | } 25 | // create the ElModal instance 26 | const reactModal = React.createElement(ElModal, props, null) 27 | 28 | // render instance and store reference once 29 | instance = ReactDOM.render(reactModal, domTarget) 30 | } 31 | 32 | // update the Modal props 33 | instance.updateProps(props) 34 | 35 | // return the instance 36 | return instance 37 | } 38 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/text/ElText.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/text/ElText.tsx 2 | import * as React from 'react' 3 | 4 | type ElTextProps = { 5 | testid?: string 6 | id?: string 7 | tag: string 8 | text: string 9 | addCss?: string 10 | } 11 | 12 | interface ComponentProps extends React.HTMLAttributes { 13 | as?: React.ElementType 14 | id?: string 15 | 'data-testid': string 16 | } 17 | 18 | const Component: React.FC = ({ as: Tag = 'p', ...otherProps }) => { 19 | return 20 | } 21 | 22 | export function ElText(props: ElTextProps) { 23 | const { id, tag, text } = props 24 | 25 | const testid = props.testid || 'testid-not-set' 26 | const addCss = (props.addCss || '').trim() 27 | 28 | // a computed property the returns the css class value of this component root element 29 | const cssClass = (): string => { 30 | const cssClasses = ['p-1'] 31 | if ((addCss || '').trim().length > 0) { 32 | cssClasses.push(addCss.trim()) 33 | } 34 | return cssClasses.join(' ').trim() 35 | } 36 | 37 | return ( 38 | 39 | {text} 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /my-react-project/src/components/primitives/toggles/ElToggle.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/primitives/toggles/ElToggle.tsx 2 | type ElToggleProps = { 3 | testid?: string 4 | id?: string 5 | checked?: boolean 6 | disabled?: boolean 7 | addCss?: string 8 | onClicked?: Function 9 | } 10 | 11 | export function ElToggle(props: ElToggleProps) { 12 | const { id, onClicked } = props 13 | 14 | const testid = props.testid || 'testid-not-set' 15 | const disabled = props.disabled || false 16 | const checked = props.checked || false 17 | const addCss = (props.addCss || '').trim() 18 | 19 | // a computed property to return a different css class based on the selected value 20 | const cssClass = (): string => { 21 | const result = [ 22 | 'relative inline-flex flex-shrink-0 h-6 w-12 border-1 rounded-full cursor-pointer transition-colors duration-200 focus:outline-none' 23 | ] 24 | if (checked) { 25 | result.push('bg-green-400') 26 | } else { 27 | result.push('bg-gray-300') 28 | } 29 | if (disabled) { 30 | result.push('opacity-40 cursor-not-allowed') 31 | } 32 | if (addCss.length > 0) { 33 | result.push(addCss.trim()) 34 | } 35 | return result.join(' ').trim() 36 | } 37 | 38 | const innerCssClass = (): string => { 39 | const result = [ 40 | 'bg-white shadow pointer-events-none inline-block h-6 w-6 rounded-full transform ring-0 transition duration-200' 41 | ] 42 | if (checked) { 43 | result.push('translate-x-6') 44 | } else { 45 | result.push('translate-x-0') 46 | } 47 | return result.join(' ').trim() 48 | } 49 | 50 | // click handler 51 | const handleClick = () => { 52 | // proceed only if the button is not disabled, otherwise ignore the click 53 | if (!disabled && onClicked) { 54 | onClicked(id) 55 | } 56 | } 57 | 58 | return ( 59 | 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /my-react-project/src/components/shared/Loader.component.tsx: -------------------------------------------------------------------------------- 1 | // file: Loader.component.tsx 2 | 3 | import React from 'react' 4 | 5 | // Loader component 6 | export class Loader extends React.Component { 7 | render(): React.ReactNode { 8 | return ( 9 |
    10 |
    11 |
    12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /my-react-project/src/components/shared/LocaleSelector.component.tsx: -------------------------------------------------------------------------------- 1 | // file: Loader.component.tsx 2 | 3 | import * as React from 'react' 4 | 5 | type Props = { 6 | locales: { key: string }[] 7 | currentLocale: string 8 | onLocaleClick: Function 9 | t: Function 10 | } 11 | 12 | export class LocaleSelector extends React.Component { 13 | constructor(props: Props) { 14 | super(props) 15 | } 16 | 17 | render(): React.ReactNode { 18 | const { locales, currentLocale, onLocaleClick, t } = this.props 19 | 20 | return ( 21 |
    22 | { 23 | /* loop through the locales and create a radio button for each locale */ 24 | locales.map((item) => { 25 | const radioId = `radio-locale-${item.key}` 26 | return ( 27 | 47 | ) 48 | }) 49 | } 50 |
    51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /my-react-project/src/config/config-files-map.ts: -------------------------------------------------------------------------------- 1 | // file: src/config/config-files-map.ts 2 | 3 | // import a reference to our Config interface: 4 | import { ConfigInterface } from './models/Config.interface' 5 | 6 | // individual environments configs: 7 | import configMock from './config-files/mock.json' 8 | import configLocal from './config-files/localapis.json' 9 | import configJsonServer from './config-files/jsonserver.json' 10 | import configBeta from './config-files/beta.json' 11 | import configProduction from './config-files/production.json' 12 | 13 | // example using strategy pattern: 14 | // const configFilesMap: { [key: string]: ConfigInterface } = { 15 | // mock: configMock, 16 | // local: configLocal, 17 | // beta: configBeta, 18 | // production: configProduction, 19 | // } 20 | // if (!configFilesMap[env]) { 21 | // throw Error(`Could not find config for VITE_APP_CONFIG key "${ env }"`) 22 | // } 23 | // export const config: ConfigInterface = configFilesMap[env] 24 | 25 | // example with javascript Map() 26 | export const configFilesMap: Map = new Map([ 27 | ['mock', configMock], 28 | ['localapis', configLocal], 29 | ['jsonserver', configJsonServer], 30 | ['beta', configBeta], 31 | ['production', configProduction] 32 | ]) 33 | -------------------------------------------------------------------------------- /my-react-project/src/config/config-files/beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "version": 0.1 4 | }, 5 | 6 | "httpClient": { 7 | "tokenKey": "myapp-token", 8 | "clientType": "fetch" 9 | }, 10 | 11 | "apiClient": { 12 | "type": "live" 13 | }, 14 | 15 | "items": { 16 | "apiClientOptions": { 17 | "endpoints": { 18 | "fetchItems": "/path/to/your/real/BETA/api/and-point" 19 | }, 20 | "mockDelay": 0 21 | } 22 | }, 23 | 24 | "localization": { 25 | "apiClientOptions": { 26 | "endpoints": { 27 | "fetchTranslation": "/path/to/your/real/BETA/api/and-poin/[namespace]/[key].json" 28 | }, 29 | "mockDelay": 0 30 | }, 31 | "locales": [ 32 | { "key": "en-US", "isDefault": true }, 33 | { "key": "it-IT", "isDefault": false }, 34 | { "key": "fr-FR", "isDefault": false }, 35 | { "key": "es-ES", "isDefault": false } 36 | ], 37 | "localStorageCache": { 38 | "enabled": true, 39 | "expirationInMinutes": 60 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /my-react-project/src/config/config-files/jsonserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "version": 0.103 4 | }, 5 | 6 | "httpClient": { 7 | "tokenKey": "myapp-token", 8 | "clientType": "fetch" 9 | }, 10 | 11 | "apiClient": { 12 | "type": "live" 13 | }, 14 | 15 | "items": { 16 | "apiClientOptions": { 17 | "endpoints": { 18 | "fetchItems": "/jsonserver/items" 19 | }, 20 | "mockDelay": 0 21 | } 22 | }, 23 | 24 | "localization": { 25 | "apiClientOptions": { 26 | "endpoints": { 27 | "fetchTranslation": "/jsonserver/[namespace]_[key]" 28 | }, 29 | "mockDelay": 0 30 | }, 31 | "locales": [ 32 | { "key": "en-US", "isDefault": true }, 33 | { "key": "it-IT", "isDefault": false }, 34 | { "key": "fr-FR", "isDefault": false }, 35 | { "key": "es-ES", "isDefault": false } 36 | ], 37 | "localStorageCache": { 38 | "enabled": true, 39 | "expirationInMinutes": 60 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /my-react-project/src/config/config-files/localapis.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "version": 0.1 4 | }, 5 | 6 | "httpClient": { 7 | "tokenKey": "myapp-token", 8 | "clientType": "fetch" 9 | }, 10 | 11 | "apiClient": { 12 | "type": "live" 13 | }, 14 | 15 | "items": { 16 | "apiClientOptions": { 17 | "endpoints": { 18 | "fetchItems": "http://api.localhost:4111/items" 19 | }, 20 | "mockDelay": 0 21 | } 22 | }, 23 | 24 | "localization": { 25 | "apiClientOptions": { 26 | "endpoints": { 27 | "fetchTranslation": "http://api.localhost:4111/localization/[namespace]/[key]" 28 | }, 29 | "mockDelay": 0 30 | }, 31 | "locales": [ 32 | { "key": "en-US", "isDefault": true }, 33 | { "key": "it-IT", "isDefault": false }, 34 | { "key": "fr-FR", "isDefault": false }, 35 | { "key": "es-ES", "isDefault": false } 36 | ], 37 | "localStorageCache": { 38 | "enabled": true, 39 | "expirationInMinutes": 60 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /my-react-project/src/config/config-files/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "version": 0.103 4 | }, 5 | 6 | "httpClient": { 7 | "tokenKey": "myapp-token", 8 | "clientType": "fetch" 9 | }, 10 | 11 | "apiClient": { 12 | "type": "mock" 13 | }, 14 | 15 | "items": { 16 | "apiClientOptions": { 17 | "endpoints": { 18 | "fetchItems": "/static/mock-data/items/items.json" 19 | }, 20 | "mockDelay": 500 21 | } 22 | }, 23 | 24 | "localization": { 25 | "apiClientOptions": { 26 | "endpoints": { 27 | "fetchTranslation": "/static/mock-data/localization/[namespace]/[key].json" 28 | }, 29 | "mockDelay": 500 30 | }, 31 | "locales": [ 32 | { "key": "en-US", "isDefault": true }, 33 | { "key": "it-IT", "isDefault": false }, 34 | { "key": "fr-FR", "isDefault": false }, 35 | { "key": "es-ES", "isDefault": false } 36 | ], 37 | "localStorageCache": { 38 | "enabled": true, 39 | "expirationInMinutes": 60 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /my-react-project/src/config/config-files/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "version": 0.1 4 | }, 5 | 6 | "httpClient": { 7 | "tokenKey": "myapp-token", 8 | "clientType": "fetch" 9 | }, 10 | 11 | "apiClient": { 12 | "type": "live" 13 | }, 14 | 15 | "items": { 16 | "apiClientOptions": { 17 | "endpoints": { 18 | "fetchItems": "/path/to/your/real/PRODUCTION/api/and-point" 19 | }, 20 | "mockDelay": 0 21 | } 22 | }, 23 | 24 | "localization": { 25 | "apiClientOptions": { 26 | "endpoints": { 27 | "fetchTranslation": "path/to/your/real/PRODUCTION/api/and-point" 28 | }, 29 | "mockDelay": 0 30 | }, 31 | "locales": [ 32 | { "key": "en-US", "isDefault": true }, 33 | { "key": "it-IT", "isDefault": false }, 34 | { "key": "fr-FR", "isDefault": false }, 35 | { "key": "es-ES", "isDefault": false } 36 | ], 37 | "localStorageCache": { 38 | "enabled": true, 39 | "expirationInMinutes": 60 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /my-react-project/src/config/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/config/index.ts 2 | // returns appropriate config based on env VITE_APP_CONFIG 3 | 4 | // import a reference to our Config interface: 5 | import { ConfigInterface } from './models/Config.interface' 6 | 7 | // import reference to configFilesMap 8 | import { configFilesMap } from './config-files-map' 9 | 10 | // import reference to our getAppConfigKey helper function 11 | import { getAppConfigKey } from './utils' 12 | 13 | // optional: you can console.log the content of import.meta.env to inspect its value: 14 | console.log(`------ env ---- "${getAppConfigKey()}"`) 15 | 16 | if (!configFilesMap.has(getAppConfigKey())) { 17 | throw Error(`Could not find config for VITE_APP_CONFIG key "${getAppConfigKey()}"`) 18 | } 19 | 20 | export const config = configFilesMap.get(getAppConfigKey()) as ConfigInterface 21 | -------------------------------------------------------------------------------- /my-react-project/src/config/models/Config.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/config/models/Config.interface.ts 2 | 3 | import { ItemsApiClientOptions, LocalizationApiClientOptions } from '../../api-client/models' 4 | 5 | export interface HttpClientConfigInterface { 6 | tokenKey: string 7 | clientType: string 8 | } 9 | 10 | /** 11 | * @Name ConfigInterface 12 | * @description 13 | * Describes the structure of a configuration file 14 | */ 15 | export interface ConfigInterface { 16 | global: { 17 | // ... things that are not specific to a single app domain 18 | version: number 19 | } 20 | 21 | httpClient: HttpClientConfigInterface 22 | 23 | apiClient: { 24 | type: string 25 | } 26 | 27 | items: { 28 | apiClientOptions: ItemsApiClientOptions 29 | } 30 | 31 | localization: { 32 | apiClientOptions: LocalizationApiClientOptions 33 | locales: { key: string; isDefault: boolean }[] 34 | localStorageCache: { enabled: boolean; expirationInMinutes: number } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /my-react-project/src/config/utils.ts: -------------------------------------------------------------------------------- 1 | // file: src/config/utils.ts 2 | 3 | // helper to read the value of REACT_APP_CONFIG (or VITE_API_CLIENT if using vite) 4 | export function getAppConfigKey() { 5 | // if using (webpack): 6 | // let env: string = 'mock' 7 | // // @ts-ignore 8 | // if (process.env && process.env.REACT_APP_CONFIG) { 9 | // // @ts-ignore 10 | // env = process.env.REACT_APP_CONFIG.trim() 11 | // } 12 | // return env 13 | 14 | // Note: Vite uses import.meta.env (reference: https://vitejs.dev/guide/env-and-mode.html) 15 | // optional: you can console.log the content of import.meta.env to inspect its values like this: console.log('import.meta.env', JSON.stringify(import.meta.env)) 16 | // @ts-ignore 17 | return (import.meta.env.VITE_APP_CONFIG || '').trim() 18 | } 19 | -------------------------------------------------------------------------------- /my-react-project/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /my-react-project/src/http-client/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/index.ts 2 | 3 | import { HttpClientInterface } from './models/HttpClient.interface' 4 | import { config } from '@/config' 5 | 6 | import { HttpClientAxios } from './models/HttpClient.axios' 7 | import { HttpClientFetch } from './models/HttpClient.fetch' 8 | 9 | // export all our interfaces/models/enums 10 | export * from './models' 11 | 12 | let _httpClient: HttpClientInterface | undefined = undefined 13 | 14 | // export out hook 15 | export const useHttpClient = () => { 16 | if (!_httpClient) { 17 | // export instance of HttpClientInterface 18 | const clientType = config.httpClient.clientType // 'fetch' // config.httpClient.clientType 19 | 20 | // if you'd like to use axios, set "clientType": "axios" within the config files --- within "httpClient" object 21 | if (clientType === 'fetch') { 22 | _httpClient = new HttpClientFetch() 23 | } else if (clientType === 'axios') { 24 | _httpClient = new HttpClientAxios() 25 | } 26 | } 27 | 28 | return _httpClient as HttpClientInterface 29 | } 30 | -------------------------------------------------------------------------------- /my-react-project/src/http-client/models/Constants.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/Constants.ts 2 | 3 | /** 4 | * @name HttpRequestType 5 | * @description 6 | * The type of http request we need to execute in our HttpClient request method 7 | */ 8 | export const enum HttpRequestType { 9 | get, 10 | post, 11 | put, 12 | delete, 13 | patch 14 | } 15 | 16 | // http content types 17 | export const HttpContentTypes = Object.freeze({ 18 | applicationJson: 'application/json', 19 | formUrlEncoded: 'application/x-www-form-urlencoded;charset=UTF-8' 20 | }) 21 | 22 | // constant for http request methods names 23 | export const HttpRequestMethods = Object.freeze({ 24 | get: 'GET', 25 | post: 'POST', 26 | put: 'PUT', 27 | delete: 'DELETE', 28 | patch: 'PATCH' 29 | }) 30 | -------------------------------------------------------------------------------- /my-react-project/src/http-client/models/HttpClient.interface.ts: -------------------------------------------------------------------------------- 1 | // files: src/http-client/models/HttpClient.interface.ts 2 | 3 | import { HttpRequestParamsInterface } from './HttpRequestParams.interface' 4 | 5 | /** 6 | * @name HttpClientConfigInterface 7 | * @description 8 | * We'll drive the HttpClient from configuration in later chapters. 9 | */ 10 | export interface HttpClientConfigInterface { 11 | tokenKey: string 12 | clientType: string 13 | } 14 | 15 | /** 16 | * @name HttpClientInterface 17 | * @description 18 | * Represents our HttpClient. 19 | */ 20 | export interface HttpClientInterface { 21 | /** 22 | * @name request 23 | * @description 24 | * A method that executes different types of http requests (i.e. GET/POST/etc) 25 | * based on the parameters argument. 26 | * The type R specify the type of the result returned 27 | * The type P specify the type of payload if any 28 | * @returns A Promise as the implementation of this method will be async. 29 | */ 30 | request(parameters: HttpRequestParamsInterface

    ): Promise 31 | } 32 | -------------------------------------------------------------------------------- /my-react-project/src/http-client/models/HttpRequestParams.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/HttpRequestParams.interface.ts 2 | 3 | import { HttpRequestType } from './Constants' 4 | 5 | /** 6 | * @name HttpRequestParamsInterface 7 | * @description 8 | * Interface represents an object we'll use to pass arguments into our HttpClient request method. 9 | * This allow us to specify the type of request we want to execute, the end-point url, 10 | * if the request should include an authentication token, and an optional payload (if POST or PUT for example) 11 | */ 12 | export interface HttpRequestParamsInterface

    { 13 | requestType: HttpRequestType 14 | endpoint: string 15 | requiresToken: boolean 16 | headers?: { [key: string]: string } 17 | payload?: P 18 | mockDelay?: number 19 | } 20 | -------------------------------------------------------------------------------- /my-react-project/src/http-client/models/UrlUtils.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/UrlUtils.ts 2 | export interface UrlUtilsInterface { 3 | getFullUrlWithParams(baseUrl: string, params: { [key: string]: number | string }): string 4 | } 5 | 6 | export const UrlUtils: UrlUtilsInterface = { 7 | /** 8 | * @name getFullUrlWithParams 9 | * @description Returns the full formatted url for an API end-point 10 | * by replacing parameters place holder with the actual values. 11 | * @param baseUrl The base API end-point witht he params placeholders like {projectId} 12 | * @param params The request params object with the key/value entries for each parameter 13 | * @returns The fully formatted API end-point url with the actual parameter values 14 | */ 15 | getFullUrlWithParams: (baseUrl: string, params: { [key: string]: number | string }): string => { 16 | const keys: string[] = Object.keys(params || {}) 17 | if ((baseUrl || '').indexOf('[') === -1 || keys.length === 0) { 18 | return baseUrl 19 | } 20 | let fullUrl = baseUrl 21 | keys.forEach((key) => { 22 | fullUrl = fullUrl.replace(`[${key}]`, (params[key] || 'null').toString()) 23 | }) 24 | return fullUrl 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /my-react-project/src/http-client/models/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/index.ts 2 | 3 | export * from './Constants' 4 | export * from './HttpClient.axios' 5 | export * from './HttpClient.fetch' 6 | export * from './HttpClient.interface' 7 | export * from './HttpRequestParams.interface' 8 | export * from './UrlUtils' 9 | -------------------------------------------------------------------------------- /my-react-project/src/icons/Moon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const MoonIcon = () => ( 4 | 5 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /my-react-project/src/localization/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/localization/index.ts 2 | 3 | import { useDateTimeFormatters, useNumberFormatters } from '@builtwithjavascript/formatters' 4 | 5 | export * from './useLocalization' 6 | export { useDateTimeFormatters, useNumberFormatters } 7 | -------------------------------------------------------------------------------- /my-react-project/src/localization/useLocalization.ts: -------------------------------------------------------------------------------- 1 | // file: src/localization/useLocalization.ts 2 | 3 | import { useTranslation } from 'react-i18next' 4 | import i18n from 'i18next' 5 | import { config } from '@/config' 6 | 7 | // import references to our localeStorage helpers: 8 | import { getUserPreferredLocale, setUserPreferredLocale } from './i18n.init' 9 | 10 | // useLocalization hook 11 | export function useLocalization() { 12 | // we have to invoke react-i18next's useTranslation here 13 | const instance = useTranslation('translation') 14 | 15 | return { 16 | t: instance.t, //returna the t translator function from useTranslation 17 | currentLocale: i18n.language, // return the current locale from i18n 18 | changeLocale: (lcid: string) => { 19 | // return helper method changeLocale 20 | i18n.changeLanguage(lcid) 21 | // also save the user preference 22 | setUserPreferredLocale(lcid) 23 | }, 24 | locales: config.localization.locales, // retrun vailable locales from our config 25 | getUserPreferredLocale 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /my-react-project/src/main.tsx: -------------------------------------------------------------------------------- 1 | // file: src/main.tsx 2 | import React from 'react' 3 | // import ReactDOM from 'react-dom/client' // before React 18 4 | import { createRoot } from 'react-dom/client' // from React 18 5 | // import tailwind main css file 6 | import './tailwind/app.css' 7 | import App from './App' 8 | // import a reference to our i18n initialization code: 9 | import './localization/i18n.init' 10 | 11 | const container = document.getElementById('root') 12 | // const root = ReactDOM.createRoot(container as Element) // before React 18 13 | const root = createRoot(container as Element) // from React 18 14 | root.render( 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /my-react-project/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './items' 2 | -------------------------------------------------------------------------------- /my-react-project/src/models/items/Item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ItemInterface { 2 | id: number 3 | name: string 4 | selected: boolean 5 | } 6 | -------------------------------------------------------------------------------- /my-react-project/src/models/items/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Item.interface' 2 | -------------------------------------------------------------------------------- /my-react-project/src/store/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/index.ts 2 | 3 | export * from './root' 4 | -------------------------------------------------------------------------------- /my-react-project/src/store/items/Items.slice.ts: -------------------------------------------------------------------------------- 1 | // src/store/items/Items.slice.ts 2 | 3 | // import createSlice and PayloadAction from redux toolkit 4 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 5 | 6 | // import out items state interface, and the item interface 7 | import { ItemsStateInterface } from './models' 8 | import { ItemInterface } from '@/models/items/Item.interface' 9 | 10 | // create an object that represents our initial items state 11 | const initialItemsState: ItemsStateInterface = { 12 | loading: false, 13 | items: [] 14 | } 15 | 16 | // create the itemsStoreSlice with createSlice: 17 | export const itemsStoreSlice = createSlice({ 18 | name: 'itemsStoreSlice', 19 | initialState: initialItemsState, 20 | reducers: { 21 | // reducers are functions that commit final mutations to the state 22 | // These will commit final mutation/changes to the state 23 | 24 | setLoading: (state, action: PayloadAction) => { 25 | state.loading = action.payload 26 | }, 27 | 28 | setItems: (state, action: PayloadAction) => { 29 | // update our state: 30 | // set our items 31 | state.items = action.payload || [] 32 | // set loading to false so the loader will be hidden in the UI 33 | state.loading = false 34 | }, 35 | 36 | setItemSelected: (state, action: PayloadAction) => { 37 | const item = action.payload 38 | const found = state.items.find((o) => o.id === item.id) as ItemInterface 39 | found.selected = !found.selected 40 | } 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /my-react-project/src/store/items/Items.store.ts: -------------------------------------------------------------------------------- 1 | // src/store/items/Items.store.ts 2 | 3 | // import hooks useSelector and useDispatch from react-redux 4 | import { useSelector } from 'react-redux' 5 | import { Dispatch } from 'react' 6 | 7 | // import a reference to our RootStateInterface 8 | import { RootStateInterface } from '@/store/root' 9 | // import a reference to our ItemInterface 10 | import { ItemInterface } from '@/models/items/Item.interface' 11 | 12 | // import a refence to our itemsStoreSlice 13 | import { itemsStoreSlice } from './Items.slice' 14 | 15 | // import a reference to our apiClient instance 16 | import { apiClient } from '@/api-client' 17 | 18 | /** 19 | * @name useItemsActions 20 | * @description 21 | * Actions hook that allows us to invoke the Items store actions from our components 22 | */ 23 | export function useItemsActions(commit: Dispatch) { 24 | // get a reference to our slice actions (which are really our mutations/commits) 25 | const mutations = itemsStoreSlice.actions 26 | 27 | // our items store actions implementation: 28 | const actions = { 29 | loadItems: async () => { 30 | // set loading to true 31 | commit(mutations.setLoading(true)) 32 | 33 | // invoke our API cient fetchItems to load the data from an API end-point 34 | const data = await apiClient.items.fetchItems() 35 | 36 | // commit our mutations by setting state.items to the data loaded 37 | commit(mutations.setItems(data)) 38 | }, 39 | toggleItemSelected: async (item: ItemInterface) => { 40 | console.log('ItemsStore: action: toggleItemSelected', item) 41 | commit(mutations.setItemSelected(item)) 42 | } 43 | } 44 | 45 | // return our store actions 46 | return actions 47 | } 48 | 49 | // hook to allows us to consume read-only state properties from our components 50 | export function useItemsGetters() { 51 | // return our store getters 52 | return { 53 | loading: useSelector((s: RootStateInterface) => s.itemsState.loading), 54 | items: useSelector((s: RootStateInterface) => s.itemsState.items) 55 | } 56 | } 57 | 58 | /** 59 | * @name ItemsStoreInterface 60 | * @description Interface represents our Items store module 61 | */ 62 | export interface ItemsStoreInterface { 63 | actions: ReturnType // use TS type inference 64 | getters: ReturnType // use TS type inference 65 | } 66 | -------------------------------------------------------------------------------- /my-react-project/src/store/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/items/index.ts 2 | 3 | export * from './Items.slice' 4 | export * from './Items.store' 5 | -------------------------------------------------------------------------------- /my-react-project/src/store/items/models/ItemsState.interface.ts: -------------------------------------------------------------------------------- 1 | // file: ItemsState.interface.ts 2 | 3 | import { ItemInterface } from '@/models/items/Item.interface' 4 | 5 | /** 6 | * @name ItemsStateInterface 7 | * @description Interface represnets our Items state 8 | */ 9 | export interface ItemsStateInterface { 10 | loading: boolean 11 | items: ItemInterface[] 12 | } 13 | -------------------------------------------------------------------------------- /my-react-project/src/store/items/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ItemsState.interface' 2 | -------------------------------------------------------------------------------- /my-react-project/src/store/root/Root.store.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/Root.store.ts 2 | 3 | // import configureStore from redux toolkit 4 | import { configureStore } from '@reduxjs/toolkit' 5 | import { useDispatch } from 'react-redux' 6 | 7 | // import our root store interface 8 | import { RootStoreInterface } from './models' 9 | 10 | // import our modules slices and actions/getters 11 | import { itemsStoreSlice, useItemsActions, useItemsGetters } from '@/store/items/' 12 | 13 | // configure root redux store for the whole app. 14 | // this will be consumed by App.tsx 15 | export const rootStore = configureStore({ 16 | reducer: { 17 | // add reducers here 18 | itemsState: itemsStoreSlice.reducer 19 | // keep adding more domain-specific reducers here as needed 20 | } 21 | }) 22 | 23 | // Infer the `RootStateInterface` type from the store itself (rootStore.getState) 24 | // thus avoiding to explicitely having to create an additional interface for the 25 | export type RootStateInterface = ReturnType 26 | 27 | // hook that returns our root store instance and will allow us to consume our app store from our components 28 | export function useAppStore(): RootStoreInterface { 29 | // note: we are callin dispatch "commit" here, as it make more sense to call it this way 30 | // feel free to just call it dispatch if you prefer 31 | const commit = useDispatch() 32 | 33 | return { 34 | itemsStore: { 35 | actions: useItemsActions(commit), 36 | getters: useItemsGetters() 37 | } 38 | // additional domain store modules will be added here as needed 39 | } 40 | } 41 | 42 | // infer the type of the entire app state 43 | type IAppState = ReturnType 44 | 45 | /** 46 | * @name getAppState 47 | * @description 48 | * Returns a snapshot of the current app state (non-reactive) 49 | * This will be used mainly across store modules (i.e. items/etc) 50 | * In components we'll usually use getters, not this. 51 | * @returns 52 | */ 53 | export function getAppState(): IAppState { 54 | const appState = rootStore.getState() 55 | return { 56 | ...appState 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /my-react-project/src/store/root/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/index.ts 2 | 3 | export * from './Root.store' 4 | -------------------------------------------------------------------------------- /my-react-project/src/store/root/models/RootStore.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/models/RootStore.interface.ts 2 | 3 | import { ItemsStoreInterface } from '@/store/items' 4 | // additional domain store interfaces will be imported here as needed 5 | 6 | /** 7 | * @name RootStoreInterface 8 | * @description Interface represents our global state manager (store) 9 | */ 10 | export interface RootStoreInterface { 11 | itemsStore: ItemsStoreInterface 12 | // additional domain store modules will be added here as needed 13 | } 14 | -------------------------------------------------------------------------------- /my-react-project/src/store/root/models/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/models/index.ts 2 | 3 | export * from './RootStore.interface' 4 | -------------------------------------------------------------------------------- /my-react-project/src/tailwind/app.css: -------------------------------------------------------------------------------- 1 | /* file: src/tailwind/app.css */ 2 | @import 'tailwindcss/base'; 3 | @import 'tailwindcss/components'; 4 | @import 'tailwindcss/utilities'; 5 | 6 | @import './other.css'; 7 | -------------------------------------------------------------------------------- /my-react-project/src/tailwind/other.css: -------------------------------------------------------------------------------- 1 | /* file: src/tailwind/other.css */ 2 | 3 | ul { 4 | list-style-type: none; 5 | margin-block-start: 0; 6 | margin-block-end: 0; 7 | margin-inline-start: 0px; 8 | margin-inline-end: 0px; 9 | padding-inline-start: 0px; 10 | } 11 | li.item { 12 | padding: 5px; 13 | outline: solid 1px #eee; 14 | display: flex; 15 | align-items: center; 16 | height: 30px; 17 | cursor: pointer; 18 | transition: background-color 0.3s ease; 19 | } 20 | li.item .name { 21 | margin-left: 6px; 22 | } 23 | li.item .selected-indicator { 24 | font-size: 2em; 25 | line-height: 0.5em; 26 | margin: 10px 8px 0 8px; 27 | color: lightgray; 28 | } 29 | li.item.selected .selected-indicator { 30 | color: skyblue; 31 | } 32 | li.item:hover { 33 | background-color: #eee; 34 | } 35 | 36 | /* begin: loader component */ 37 | .loader { 38 | display: inline-block; 39 | } 40 | .loader .bounceball { 41 | position: relative; 42 | width: 30px; 43 | } 44 | .loader .bounceball:before { 45 | position: absolute; 46 | content: ''; 47 | top: 0; 48 | width: 30px; 49 | height: 30px; 50 | border-radius: 50%; 51 | background-color: #61dafa; 52 | transform-origin: 50%; 53 | animation: bounce 500ms alternate infinite ease; 54 | } 55 | @keyframes bounce { 56 | 0% { 57 | top: 60px; 58 | height: 10px; 59 | border-radius: 60px 60px 20px 20px; 60 | transform: scaleX(2); 61 | } 62 | 25% { 63 | height: 60px; 64 | border-radius: 50%; 65 | transform: scaleX(1); 66 | } 67 | 100% { 68 | top: 0; 69 | } 70 | } 71 | /* end: loader component */ 72 | -------------------------------------------------------------------------------- /my-react-project/src/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | import { render } from '@testing-library/react' 3 | 4 | const customRender = (ui: React.ReactElement, options = {}) => 5 | render(ui, { 6 | // wrap provider(s) here if needed 7 | wrapper: ({ children }) => children, 8 | ...options 9 | }) 10 | 11 | export * from '@testing-library/react' 12 | export { default as userEvent } from '@testing-library/user-event' 13 | // override render export 14 | export { customRender as render } 15 | -------------------------------------------------------------------------------- /my-react-project/src/tests/unit/config/config-files-map.test.ts: -------------------------------------------------------------------------------- 1 | import { configFilesMap } from '@/config/config-files-map' 2 | 3 | describe('configsMap', () => { 4 | it('instance should have "mock" key', () => { 5 | expect(configFilesMap.has('mock')).to.equal(true) 6 | }) 7 | 8 | it('instance should have "jsonserver" key', () => { 9 | expect(configFilesMap.has('jsonserver')).to.equal(true) 10 | }) 11 | 12 | it('instance should have "localapis" key', () => { 13 | expect(configFilesMap.has('localapis')).to.equal(true) 14 | }) 15 | 16 | it('instance should have "beta" key', () => { 17 | expect(configFilesMap.has('beta')).to.equal(true) 18 | }) 19 | 20 | it('instance should have "production" key', () => { 21 | expect(configFilesMap.has('production')).to.equal(true) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /my-react-project/src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts: -------------------------------------------------------------------------------- 1 | // file: src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts 2 | 3 | import { UrlUtils } from '@/http-client' 4 | 5 | describe('UrlUtils: getFullUrlWithParams', () => { 6 | it('should return fullUrl formatted as expected with one param', () => { 7 | const endpoint = 'https://unit-test-api/v1/domain/[catalogId]/[partId]' 8 | const params = { 9 | catalogId: 5346782, 10 | partId: 'abcde23' 11 | } 12 | const result = UrlUtils.getFullUrlWithParams(endpoint, params) 13 | 14 | expect('https://unit-test-api/v1/domain/5346782/abcde23').toEqual(result) 15 | }) 16 | 17 | // test our component click event 18 | it('should return fullUrl formatted as expected with multiple params', () => { 19 | const endpoint = 'https://unit-test-api/v1/domain/[country]/[state]/[cityId]' 20 | const params = { 21 | country: 'USA', 22 | state: 'NY', 23 | cityId: 'gtref345ytr' 24 | } 25 | const result = UrlUtils.getFullUrlWithParams(endpoint, params) 26 | 27 | expect('https://unit-test-api/v1/domain/USA/NY/gtref345ytr').toEqual(result) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /my-react-project/src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts: -------------------------------------------------------------------------------- 1 | // file: src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts 2 | 3 | import axios from 'axios' 4 | import { HttpClientAxios, HttpRequestType, HttpRequestParamsInterface } from '@/http-client' 5 | 6 | let mockRequestParams: HttpRequestParamsInterface = { 7 | requestType: HttpRequestType.get, 8 | endpoint: 'path/to/a/get/api/endpoint', 9 | requiresToken: false 10 | } 11 | 12 | describe('HttpClient: axios-client: request: get', () => { 13 | const httpClient = new HttpClientAxios() 14 | 15 | it('should execute get request succesfully', () => { 16 | vitest 17 | .spyOn(axios, 'get') 18 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${mockRequestParams.endpoint}` })) 19 | 20 | httpClient 21 | .request(mockRequestParams) 22 | .then((response) => { 23 | //console.debug('response:', response) 24 | expect(response).toEqual(`request completed: ${mockRequestParams.endpoint}`) 25 | }) 26 | .catch((error) => { 27 | console.info('AxiosClient.request.get.test.ts: error', error) 28 | }) 29 | }) 30 | 31 | it('get should throw error on rejection', () => { 32 | vitest 33 | .spyOn(axios, 'get') 34 | .mockImplementation(async () => Promise.reject({ data: `request completed: ${mockRequestParams.endpoint}` })) 35 | 36 | httpClient.request(mockRequestParams).catch((error) => { 37 | expect(error).toBeDefined() 38 | expect(error.toString()).toEqual('Error: HttpClientAxios: exception') 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /my-react-project/src/tests/unit/http-client/axios-client/AxiosClient.request.post.test.ts: -------------------------------------------------------------------------------- 1 | // file: src/tests/unit/http-client/axios-client/AxiosClient.request.post.test.ts 2 | 3 | import axios from 'axios' 4 | import { HttpClientAxios, HttpRequestType, HttpRequestParamsInterface } from '@/http-client' 5 | 6 | let mockRequestParams: HttpRequestParamsInterface = { 7 | requestType: HttpRequestType.post, 8 | endpoint: 'path/to/a/post/api/endpoint', 9 | requiresToken: false, 10 | payload: {} 11 | } 12 | 13 | type P = typeof mockRequestParams.payload 14 | 15 | describe('HttpClient: axios-client: request: post', () => { 16 | const httpClient = new HttpClientAxios() 17 | 18 | it('should execute post request succesfully', () => { 19 | vitest 20 | .spyOn(axios, 'post') 21 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${mockRequestParams.endpoint}` })) 22 | 23 | httpClient 24 | .request(mockRequestParams) 25 | .then((response) => { 26 | //console.debug('response:', response) 27 | expect(response).toEqual(`request completed: ${mockRequestParams.endpoint}`) 28 | }) 29 | .catch((error) => { 30 | console.info('AxiosClient.request.post.test.ts: post error', error) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /my-react-project/src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts: -------------------------------------------------------------------------------- 1 | // file: src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts 2 | 3 | import { HttpClientFetch, HttpRequestType, HttpRequestParamsInterface, HttpRequestMethods } from '@/http-client' 4 | 5 | let mockRequestParams: HttpRequestParamsInterface = { 6 | requestType: HttpRequestType.get, 7 | endpoint: 'path/to/a/get/api/endpoint', 8 | requiresToken: false 9 | } 10 | 11 | describe('HttpClient: axios-client: request: get', (done) => { 12 | const httpClient = new HttpClientFetch() 13 | 14 | it('should execute get request succesfully', async () => { 15 | // could not find an easy way to use spyOn for fetch so overriding global.fetch 16 | // save original fetch 17 | const unmockedFetch = global.fetch || (() => {}) 18 | global.fetch = unmockedFetch 19 | 20 | const expectedResult = JSON.stringify({ 21 | result: `request completed: ${mockRequestParams.endpoint}` 22 | }) 23 | 24 | vitest.spyOn(global, 'fetch').mockImplementation(async () => 25 | Promise.resolve({ 26 | redirected: false, 27 | json: () => Promise.resolve(expectedResult) 28 | } as any) 29 | ) 30 | 31 | try { 32 | const response = await httpClient.request(mockRequestParams) 33 | expect(response).not.toBeNull() 34 | expect(response).toEqual(expectedResult) 35 | } catch (error) { 36 | console.info('FetchClient.request.get.test.ts: error', error) 37 | } 38 | 39 | // restore globa.fetch 40 | global.fetch = unmockedFetch 41 | }) 42 | 43 | it('get should throw error on rejection', () => { 44 | // could not find an easy way to use spyOn for fetch so overriding global.fetch 45 | // save original fetch 46 | const unmockedFetch = global.fetch || (() => {}) 47 | global.fetch = unmockedFetch 48 | 49 | vitest.spyOn(global, 'fetch').mockImplementation(async () => Promise.reject()) 50 | 51 | httpClient.request(mockRequestParams).catch((error) => { 52 | expect(error).toBeDefined() 53 | expect(error.toString()).toEqual('Error: HttpClientFetch: exception') 54 | }) 55 | 56 | // restore globa.fetch 57 | global.fetch = unmockedFetch 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /my-react-project/src/views/Items.view.tsx: -------------------------------------------------------------------------------- 1 | // file: src/views/Items.view.tsx 2 | import * as React from 'react' 3 | 4 | // import hook useEffect from react 5 | import { useEffect } from 'react' 6 | // import a reference to our ItemInterface 7 | import { ItemInterface } from '@/models/items/Item.interface' 8 | // import a reference to your ItemsList component: 9 | import { ItemsListComponent } from '@/components/items/ItemsList.component' 10 | // import our useAppStore hook from our store 11 | import { useAppStore } from '@/store' 12 | 13 | // ItemsView component: 14 | function ItemsView() { 15 | // get a reference to our itemsStore instanceusing our useAppStore() hook: 16 | const { itemsStore } = useAppStore() 17 | 18 | // get a reference to the items state data through our itemsStore getters: 19 | const { loading, items } = itemsStore.getters 20 | 21 | // item select event handler 22 | const onItemSelect = (item: ItemInterface) => { 23 | itemsStore.actions.toggleItemSelected(item) 24 | } 25 | 26 | // use React useEffect to invoke our itemsStore loadItems action only once after this component is rendered: 27 | useEffect(() => { 28 | itemsStore.actions.loadItems() 29 | }, []) // <-- empty array means 'run once' 30 | 31 | // return our render function containing our ItemslistComponent as we did earlier in the App.tsx file 32 | return ( 33 |

    34 | 35 |
    36 | ) 37 | } 38 | 39 | export default ItemsView 40 | -------------------------------------------------------------------------------- /my-react-project/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // file: src/vite-env.d.ts 2 | 3 | /// 4 | /// 5 | 6 | // types for Vite env variables: 7 | // (reference: https://vitejs.dev/guide/env-and-mode.html#intellisense-for-typescript) 8 | interface ImportMetaEnv { 9 | readonly VITE_APP_CONFIG: string 10 | // more env variables... 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv 15 | } 16 | -------------------------------------------------------------------------------- /my-react-project/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{html,js,ts,tsx}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /my-react-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "types": [ 24 | "react", 25 | "vite/client", 26 | "vitest/globals" 27 | ] 28 | }, 29 | "include": ["./src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /my-react-project/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": [ 8 | "vite.config.jsonserver.ts", 9 | "vite.config.mock.ts", 10 | "vite.config.production.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /my-react-project/vite.config.jsonserver.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite' 5 | import react from '@vitejs/plugin-react' 6 | import { fileURLToPath, URL } from 'url' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [react()], 11 | envDir: './src/', 12 | resolve: { 13 | alias: { 14 | // @ts-ignore 15 | '@': fileURLToPath(new URL('./src', import.meta.url)), 16 | }, 17 | }, 18 | server: { 19 | port: 3000, 20 | origin: 'http://localhost:3000', 21 | open: 'http://localhost:3000', 22 | proxy: { 23 | '/jsonserver': { 24 | target: 'http://localhost:3111', 25 | changeOrigin: true, 26 | secure: false, 27 | ws: false, 28 | rewrite: (path) => path.replace(/^\/jsonserver/, '') 29 | } 30 | } 31 | }, 32 | test: { 33 | globals: true, 34 | environment: 'jsdom', 35 | exclude: [ 36 | 'node_modules' 37 | ] 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /my-react-project/vite.config.mock.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite' 5 | import react from '@vitejs/plugin-react' 6 | import { fileURLToPath, URL } from 'url' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [react()], 11 | envDir: './src/', 12 | resolve: { 13 | alias: { 14 | // @ts-ignore 15 | '@': fileURLToPath(new URL('./src', import.meta.url)), 16 | }, 17 | }, 18 | server: { 19 | port: 3000, 20 | origin: 'http://localhost:3000', 21 | open: 'http://localhost:3000' 22 | }, 23 | test: { 24 | globals: true, 25 | environment: 'jsdom', 26 | exclude: [ 27 | 'node_modules' 28 | ] 29 | }, 30 | define: { 31 | __VUE_I18N_LEGACY_API__: true, 32 | __VUE_I18N_FULL_INSTALL__: true, 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /my-react-project/vite.config.production.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite' 5 | import react from '@vitejs/plugin-react' 6 | import { fileURLToPath, URL } from 'url' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [react()], 11 | envDir: './src/', 12 | resolve: { 13 | alias: { 14 | // @ts-ignore 15 | '@': fileURLToPath(new URL('./src', import.meta.url)), 16 | }, 17 | }, 18 | test: { 19 | globals: true, 20 | environment: 'jsdom', 21 | exclude: [ 22 | 'node_modules' 23 | ] 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /readme-images/react-typescript-300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/readme-images/react-typescript-300.png -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-01", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.17", 17 | "@types/react-dom": "^18.0.6", 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "typescript": "^4.6.4", 20 | "vite": "^3.1.0" 21 | } 22 | } -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import reactLogo from './assets/react.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 |
    10 | 18 |

    Vite + React

    19 |
    20 | 23 |

    24 | Edit src/App.tsx and save to test HMR 25 |

    26 |
    27 |

    28 | Click on the Vite and React logos to learn more 29 |

    30 |
    31 | ) 32 | } 33 | 34 | export default App 35 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 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 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-01/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-02", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.17", 17 | "@types/react-dom": "^18.0.6", 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "typescript": "^4.6.4", 20 | "vite": "^3.1.0" 21 | } 22 | } -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/App.tsx: -------------------------------------------------------------------------------- 1 | // file: src/App.tsx 2 | 3 | // import reference to your ItemsList component: 4 | import { ItemsListComponent } from './components/items/ItemsList.component' 5 | 6 | // mock data: 7 | const items: any[] = [{ 8 | id: 1, 9 | name: 'Item 1' 10 | }, { 11 | id: 2, 12 | name: 'Item 2' 13 | }, { 14 | id: 3, 15 | name: 'Item 3' 16 | }] 17 | 18 | // component: 19 | function App() { 20 | 21 | return ( 22 |
    23 | 24 |
    25 | ); 26 | } 27 | 28 | export default App 29 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/components/items/ItemsList.component.tsx: -------------------------------------------------------------------------------- 1 | // example using const of type React.FC: 2 | import React from 'react' 3 | 4 | export const ItemsListComponent: React.FC<{ 5 | items: any[] 6 | }> = (props) => { 7 | 8 | return ( 9 |
    10 |

    Items:

    11 |
      12 | { 13 | props.items.map((item, index) =>
    • {item.name}
    • 14 | ) 15 | } 16 |
    17 |
    18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/components/items/ItemsList.with-class.component.tsx: -------------------------------------------------------------------------------- 1 | // example using class extending component 2 | import React from 'react' 3 | 4 | export class ItemsListComponent extends React.Component<{ 5 | items: any[] 6 | }> { 7 | constructor(props: { 8 | items: any[] 9 | }) { 10 | super(props) 11 | } 12 | 13 | render(): React.ReactNode { 14 | const { items } = this.props 15 | 16 | return
    17 |

    Items:

    18 |
      19 | { 20 | items.map((item: any, index: number) =>
    • {item.name}
    • ) 21 | } 22 |
    23 |
    24 | } 25 | } 26 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/main.tsx: -------------------------------------------------------------------------------- 1 | // file: src/main.tsx 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import App from './App' 6 | // import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 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 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-02/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-03", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.17", 17 | "@types/react-dom": "^18.0.6", 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "typescript": "^4.6.4", 20 | "vite": "^3.1.0" 21 | } 22 | } -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/App.tsx: -------------------------------------------------------------------------------- 1 | // file: src/App.tsx 2 | 3 | // import reference to our interface 4 | import { ItemInterface } from './models/items/Item.interface' 5 | // import reference to your ItemsList component: 6 | import { ItemsListComponent } from './components/items/ItemsList.component' 7 | 8 | // mock data: 9 | const items: ItemInterface[] = [{ // change any[] to ItemInterface[] 10 | id: 1, 11 | name: 'Item 1', 12 | selected: false // add selected: false to each item 13 | }, { 14 | id: 2, 15 | name: 'Item 2', 16 | selected: false // add selected: false to each item 17 | }, { 18 | id: 3, 19 | name: 'Item 3', 20 | selected: false // add selected: false to each item 21 | }] 22 | 23 | // component: 24 | function App() { 25 | 26 | return ( 27 |
    28 | 29 |
    30 | ); 31 | } 32 | 33 | export default App 34 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/components/items/ItemsList.component.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/ItemsList.component.tsx 2 | 3 | // example using const of type React.FC: 4 | import React from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from '../../models/items/Item.interface' 7 | 8 | export const ItemsListComponent: React.FC<{ 9 | items: ItemInterface[] // replace any[] with ItemInterface[] 10 | }> = (props) => { 11 | 12 | return ( 13 |
    14 |

    Items:

    15 |
      16 | { 17 | props.items.map((item, index) =>
    • {item.name}
    • 18 | ) 19 | } 20 |
    21 |
    22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/components/items/ItemsList.with-class-syntax.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/ItemsList.with-class-syntax.tsx 2 | 3 | // example using class extending component 4 | import React from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from '../../models/items/Item.interface' 7 | 8 | export class ItemsListComponent extends React.Component<{ 9 | items: ItemInterface[] // replace any[] with ItemInterface[] 10 | }> { 11 | constructor(props: { 12 | items: ItemInterface[] // replace any[] with ItemInterface[] 13 | }) { 14 | super(props) 15 | } 16 | 17 | render(): React.ReactNode { 18 | const { items } = this.props 19 | 20 | return
    21 |

    Items:

    22 |
      23 | { 24 | items.map((item: any, index: number) =>
    • {item.name}
    • ) 25 | } 26 |
    27 |
    28 | } 29 | } 30 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/main.tsx: -------------------------------------------------------------------------------- 1 | // file: src/main.tsx 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import App from './App' 6 | // import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/models/items/Item.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/items/Item.interface.ts 2 | 3 | export interface ItemInterface { 4 | id: number 5 | name: string 6 | selected: boolean 7 | } 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 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 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-03/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-04", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.17", 17 | "@types/react-dom": "^18.0.6", 18 | "@vitejs/plugin-react": "^2.1.0", 19 | "typescript": "^4.6.4", 20 | "vite": "^3.1.0" 21 | } 22 | } -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/App.tsx: -------------------------------------------------------------------------------- 1 | // file: src/App.tsx 2 | import { useState } from 'react' 3 | // import reference to our interface 4 | import { ItemInterface } from './models/items/Item.interface' 5 | // import reference to your ItemsList component: 6 | import { ItemsListComponent } from './components/items/ItemsList.component' 7 | 8 | 9 | // begin: remove code block: 10 | // const items: ItemInterface[] = [{ 11 | // id: 1, 12 | // name: 'Item 1', 13 | // selected: false 14 | // }, { 15 | // id: 2, 16 | // name: 'Item 2', 17 | // selected: false 18 | // }, { 19 | // id: 3, 20 | // name: 'Item 3', 21 | // selected: false 22 | // }] 23 | // end: remove code block: 24 | 25 | // component: 26 | function App() { 27 | // begin: add code block 28 | // add the useState declaration here passing our mock-data array as an argument 29 | const [items, setItems] = useState([{ 30 | id: 1, 31 | name: 'Item 1', 32 | selected: true 33 | }, { 34 | id: 2, 35 | name: 'Item 2', 36 | selected: false 37 | }, { 38 | id: 3, 39 | name: 'Item 3', 40 | selected: false 41 | }]) 42 | // end: add code block 43 | 44 | // begin: add code block 45 | const onItemSelect = (item: ItemInterface) => { 46 | const updatedItems = [...items] 47 | const found = updatedItems.find(o => o.id === item.id) as ItemInterface 48 | found.selected = !item.selected 49 | setItems(updatedItems) 50 | console.log('App.tsx: onItemSelect', found.id, found.selected, updatedItems) 51 | } 52 | // end: add code block 53 | 54 | return ( 55 |
    56 | 57 |
    58 | ); 59 | } 60 | 61 | export default App 62 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/components/items/ItemsList.component.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/ItemsList.component.tsx 2 | 3 | // example using const of type React.FC: 4 | import React from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from '../../models/items/Item.interface' 7 | 8 | type Props = { 9 | items: ItemInterface[], 10 | onItemSelect: (item: ItemInterface) => void 11 | } 12 | 13 | // NOTE: React is perfectly happy with normal function signatures so you could simply use this if you prefer: "export const ItemsListComponent = (props: Props) => { ..." 14 | export const ItemsListComponent: React.FC = (props) => { 15 | 16 | const handleItemClick = (item: ItemInterface) => { 17 | props.onItemSelect(item) 18 | // item.selected = !item.selected 19 | // console.log('handleItemClick', item.id, item.selected) 20 | } 21 | 22 | return ( 23 |
    24 |

    Items (function syntax):

    25 |
      26 | { 27 | props.items.map((item, index) => { 28 | return ( 29 |
    • handleItemClick(item)}> 31 | {item.name} [{ String(item.selected) }] {/* output item.selected next to the name */} 32 |
    • 33 | ) 34 | }) 35 | } 36 |
    37 |
    38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/components/items/ItemsList.with-class-syntax.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/ItemsList.with-class-syntax.tsx 2 | 3 | // example using class extending component 4 | import React from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from '../../models/items/Item.interface' 7 | 8 | type Props = { 9 | items: ItemInterface[], 10 | onItemSelect: (item: ItemInterface) => void 11 | } 12 | 13 | export class ItemsListComponent extends React.Component { 14 | constructor(props: Props) { 15 | super(props) 16 | } 17 | 18 | handleItemClick (item: ItemInterface) { 19 | this.props.onItemSelect(item) 20 | } 21 | 22 | render(): React.ReactNode { 23 | const { items } = this.props 24 | 25 | return ( 26 |
    27 |

    Items (class syntax):

    28 |
      29 | { 30 | items.map((item: any, index: number) =>
    • this.handleItemClick(item)}>{item.name}
    • ) 31 | } 32 |
    33 |
    34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/main.tsx: -------------------------------------------------------------------------------- 1 | // file: src/main.tsx 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import App from './App' 6 | // import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/models/items/Item.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/items/Item.interface.ts 2 | 3 | export interface ItemInterface { 4 | id: number 5 | name: string 6 | selected: boolean 7 | } 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 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 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-04/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chapter-05", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "test": "vitest run", 11 | "test-watch": "npm run test -- --watch" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@testing-library/react": "^13.4.0", 19 | "@testing-library/user-event": "^14.4.3", 20 | "@types/jest": "^29.1.1", 21 | "@types/react": "^18.0.17", 22 | "@types/react-dom": "^18.0.6", 23 | "@vitejs/plugin-react": "^2.1.0", 24 | "jsdom": "^20.0.1", 25 | "typescript": "^4.6.4", 26 | "vite": "^3.1.0", 27 | "vitest": "^0.23.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/App.css: -------------------------------------------------------------------------------- 1 | /* file: App.css */ 2 | 3 | .App { 4 | padding: 20px; 5 | } 6 | ul { 7 | padding-inline-start: 0; 8 | margin-block-start: 0; 9 | margin-block-end: 0; 10 | margin-inline-start: 0px; 11 | margin-inline-end: 0px; 12 | padding-inline-start: 0px; 13 | } 14 | li.item { 15 | padding: 5px; 16 | outline: solid 1px #eee; 17 | display: flex; 18 | align-items: center; 19 | height: 30px; 20 | cursor: pointer; 21 | transition: background-color 0.3s ease; 22 | } 23 | li.item .name { 24 | margin-left: 6px; 25 | } 26 | li.item .selected-indicator { 27 | font-size: 2em; 28 | line-height: 0.5em; 29 | margin: 10px 8px 0 8px; 30 | color: lightgray; 31 | } 32 | li.item.selected .selected-indicator { 33 | color: skyblue; 34 | } 35 | li.item:hover { 36 | background-color: #eee; 37 | } 38 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/App.tsx: -------------------------------------------------------------------------------- 1 | // file: src/App.tsx 2 | import './App.css' // <-- restore this import 3 | 4 | import { useState } from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from './models/items/Item.interface' 7 | // import reference to your ItemsList component: 8 | import { ItemsListComponent } from './components/items/ItemsList.component' 9 | import { ItemsListComponent as ItemsListClassComponent } from './components/items/ItemsList.with-class-syntax' 10 | 11 | // component: 12 | function App() { 13 | // add the useState declaration here passing our mock-data array as an argument 14 | const [items, setItems] = useState([{ 15 | id: 1, 16 | name: 'Item 1', 17 | selected: true 18 | }, { 19 | id: 2, 20 | name: 'Item 2', 21 | selected: false 22 | }, { 23 | id: 3, 24 | name: 'Item 3', 25 | selected: false 26 | }]) 27 | 28 | const onItemSelect = (item: ItemInterface) => { 29 | const updatedItems = [...items] 30 | const found = updatedItems.find(o => o.id === item.id) as ItemInterface 31 | found.selected = !item.selected 32 | setItems(updatedItems) 33 | // console.log('App.tsx: onItemSelect', found.id, found.selected, updatedItems) 34 | } 35 | 36 | return ( 37 |
    38 | 39 | 40 |
    41 | ); 42 | } 43 | 44 | export default App 45 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/components/items/ItemsList.component.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/ItemsList.component.tsx 2 | 3 | // example using const of type React.FC: 4 | import React from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from '../../models/items/Item.interface' 7 | // import reference to your Item component: 8 | import { ItemComponent } from './children/Item.component' 9 | 10 | type Props = { 11 | items: ItemInterface[], 12 | onItemSelect: (item: ItemInterface) => void 13 | } 14 | // with function syntax 15 | // NOTE: React is perfectly happy with normal function signatures so you could simply use this if you prefer: "export const ItemsListComponent = (props: Props) => { ..." 16 | export const ItemsListComponent: React.FC = (props) => { 17 | 18 | const handleItemClick = (item: ItemInterface) => { 19 | props.onItemSelect(item) 20 | } 21 | 22 | return ( 23 |
    24 |

    Items (function syntax):

    25 |
      26 | { 27 | props.items.map((item, index) => { 28 | // remove this return block: 29 | // return ( 30 | //
    • handleItemClick(item)}> 32 | // {item.name} [{ String(item.selected) }] {/* output item.selected next to the name */} 33 | //
    • 34 | // ) 35 | // add this return block: 36 | return ( 37 | handleItemClick(item)}> 38 | ) 39 | }) 40 | } 41 |
    42 |
    43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/components/items/ItemsList.with-class-syntax.tsx: -------------------------------------------------------------------------------- 1 | // file: src/components/items/ItemsList.with-class-syntax.tsx 2 | 3 | // example using class extending component 4 | import React from 'react' 5 | // import reference to our interface 6 | import { ItemInterface } from '../../models/items/Item.interface' 7 | // import reference to your Item component: 8 | import { ItemComponent } from './children/Item.component' 9 | 10 | type Props = { 11 | items: ItemInterface[], 12 | onItemSelect: (item: ItemInterface) => void 13 | } 14 | // example using class syntax 15 | export class ItemsListComponent extends React.Component { 16 | constructor(props: Props) { 17 | super(props) 18 | } 19 | 20 | handleItemClick (item: ItemInterface) { 21 | this.props.onItemSelect(item) 22 | } 23 | 24 | render(): React.ReactNode { 25 | const { items } = this.props 26 | 27 | return ( 28 |
    29 |

    Items (class syntax):

    30 |
      31 | { 32 | items.map((item: any, index: number) => { 33 | //return
    • this.handleItemClick(item)}>{item.name}
    • 34 | return this.handleItemClick(item)}> 35 | }) 36 | } 37 |
    38 |
    39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/components/items/children/Item.behavior.test.tsx: -------------------------------------------------------------------------------- 1 | // file: Item.behavior.test.tsx 2 | 3 | import { render, fireEvent, prettyDOM } from '@testing-library/react' 4 | 5 | // import reference to our interface 6 | import { ItemInterface } from '../../../models/items/Item.interface' 7 | // import reference to your Item component: 8 | import { ItemComponent } from './Item.component' 9 | 10 | describe('Item.component: behavior' , () => { 11 | 12 | // test our component click event 13 | it('click event invokes onItemSelect handler as expected', () => { 14 | const model: ItemInterface = { 15 | id: 1, 16 | name: 'Unit test item 1', 17 | selected: false 18 | } 19 | 20 | // create a spy function with vitest.fn() 21 | const onItemSelect = vitest.fn() 22 | 23 | const testid = 'unit-test-item' 24 | 25 | // render our component 26 | const { container } = render() 27 | // get a reference to the
  • element 28 | const liElement = container.firstChild as HTMLElement 29 | // fire click 30 | fireEvent.click(liElement) 31 | // check test result 32 | expect(onItemSelect).toHaveBeenCalledTimes(1) 33 | }) 34 | 35 | }) 36 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/components/items/children/Item.component.tsx: -------------------------------------------------------------------------------- 1 | // file: Item.component.tsx 2 | 3 | import React from 'react' 4 | // import reference to our interface 5 | import { ItemInterface } from '../../../models/items/Item.interface' 6 | 7 | // component props type: 8 | type Props = { 9 | testid: string 10 | model: ItemInterface, 11 | onItemSelect: (item: ItemInterface) => void 12 | } 13 | 14 | // example using class syntax 15 | export class ItemComponent extends React.Component { 16 | constructor(props: Props) { 17 | super(props) 18 | } 19 | 20 | get cssClass () { 21 | let css = 'item' 22 | if (this.props.model?.selected) { 23 | css += ' selected' 24 | } 25 | return css.trim() 26 | } 27 | 28 | handleItemClick (item: ItemInterface) { 29 | this.props.onItemSelect(item) 30 | } 31 | 32 | render(): React.ReactNode { 33 | const { model } = this.props 34 | const testid = this.props.testid || 'not-set' 35 | 36 | return ( 37 |
  • this.handleItemClick(model)}> 38 |
    *
    39 |
    {model.name}
    40 |
  • 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/main.tsx: -------------------------------------------------------------------------------- 1 | // file: src/main.tsx 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom/client' 5 | import App from './App' 6 | // import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/models/items/Item.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/items/Item.interface.ts 2 | 3 | export interface ItemInterface { 4 | id: number 5 | name: string 6 | selected: boolean 7 | } 8 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 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 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "types": [ 25 | "react", 26 | "vite/client", 27 | "vitest/globals" 28 | ] 29 | }, 30 | "include": ["src"], 31 | "references": [{ "path": "./tsconfig.node.json" }] 32 | } 33 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /steps-by-chapter/chapter-05/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from 'vite' 5 | import react from '@vitejs/plugin-react' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | react() 11 | ], 12 | test: { 13 | globals: true, 14 | environment: 'jsdom', 15 | exclude: [ 16 | 'node_modules' 17 | ] 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /with-create-react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /with-create-react-app/create-react-app-readme.bak: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /with-create-react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.7.1", 7 | "axios": "^0.25.0", 8 | "react": "^17.0.2", 9 | "react-dom": "^17.0.2", 10 | "react-redux": "^7.2.6", 11 | "react-scripts": "5.0.0", 12 | "typescript": "^4.5.5", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "cross-env REACT_APP_API_CLIENT=mock react-scripts start", 17 | "build": "cross-env REACT_APP_API_CLIENT=live react-scripts build", 18 | "test": "react-scripts test --env=jsdom --verbose=false --silent=false --watchAll", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "@testing-library/jest-dom": "^5.16.1", 41 | "@testing-library/react": "^12.1.2", 42 | "@testing-library/user-event": "^13.5.0", 43 | "@types/jest": "^27.4.0", 44 | "@types/node": "^16.11.21", 45 | "@types/react": "^17.0.38", 46 | "@types/react-dom": "^17.0.11", 47 | "cross-env": "^7.0.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /with-create-react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/with-create-react-app/public/favicon.ico -------------------------------------------------------------------------------- /with-create-react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /with-create-react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/with-create-react-app/public/logo192.png -------------------------------------------------------------------------------- /with-create-react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianof/large-scale-apps-my-react-project/5ff3eca4ceb2b5aa521a1191705779558afef146/with-create-react-app/public/logo512.png -------------------------------------------------------------------------------- /with-create-react-app/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 | -------------------------------------------------------------------------------- /with-create-react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /with-create-react-app/public/static/data/items.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": 1, 3 | "name": "Item 1", 4 | "selected": false 5 | }, { 6 | "id": 2, 7 | "name": "Item 2", 8 | "selected": false 9 | }, { 10 | "id": 3, 11 | "name": "Item 3", 12 | "selected": false 13 | }, { 14 | "id": 4, 15 | "name": "Item 4", 16 | "selected": false 17 | }, { 18 | "id": 5, 19 | "name": "Item 5", 20 | "selected": false 21 | }] 22 | -------------------------------------------------------------------------------- /with-create-react-app/src/App.css: -------------------------------------------------------------------------------- 1 | /* file: App.css */ 2 | 3 | .App { 4 | padding: 20px; 5 | } 6 | ul { 7 | list-style-type: none; 8 | margin-block-start: 0; 9 | margin-block-end: 0; 10 | margin-inline-start: 0px; 11 | margin-inline-end: 0px; 12 | padding-inline-start: 0px; 13 | } 14 | li.item { 15 | padding: 5px; 16 | outline: solid 1px #eee; 17 | display: flex; 18 | align-items: center; 19 | height: 30px; 20 | cursor: pointer; 21 | transition: background-color 0.3s ease; 22 | } 23 | li.item .name { 24 | margin-left: 6px; 25 | } 26 | li.item .selected-indicator { 27 | font-size: 2em; 28 | line-height: 0.5em; 29 | margin: 10px 8px 0 8px; 30 | color: lightgray; 31 | } 32 | li.item.selected .selected-indicator { 33 | color: skyblue; 34 | } 35 | li.item:hover { 36 | background-color: #eee; 37 | } 38 | 39 | /* begin: loader component */ 40 | .loader { 41 | display: inline-block; 42 | } 43 | .loader .bounceball { 44 | position: relative; 45 | width: 30px; 46 | } 47 | .loader .bounceball:before { 48 | position: absolute; 49 | content: ''; 50 | top: 0; 51 | width: 30px; 52 | height: 30px; 53 | border-radius: 50%; 54 | background-color: #61dafa; 55 | transform-origin: 50%; 56 | animation: bounce 500ms alternate infinite ease; 57 | } 58 | @keyframes bounce { 59 | 0% { 60 | top: 60px; 61 | height: 10px; 62 | border-radius: 60px 60px 20px 20px; 63 | transform: scaleX(2); 64 | } 65 | 25% { 66 | height: 60px; 67 | border-radius: 50%; 68 | transform: scaleX(1); 69 | } 70 | 100% { 71 | top: 0; 72 | } 73 | } 74 | /* end: loader component */ -------------------------------------------------------------------------------- /with-create-react-app/src/App.test.bak: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /with-create-react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | // file: App.tsx 2 | 3 | // import our app.css 4 | import './App.css' 5 | 6 | // import a reference to Redux Proivder and our globalStore 7 | import { Provider } from 'react-redux' 8 | import { globalStore} from './store' 9 | 10 | // import a reference to our ItemsView component 11 | import ItemsView from './views/Items.view' 12 | 13 | // App component: 14 | function App() { 15 | return ( 16 | {/* wrap the root App element with Redux store provider */} 17 |
    18 | 19 |
    20 |
    21 | ) 22 | } 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /with-create-react-app/src/api-client/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/index.ts 2 | 3 | import { ApiClientInterface } from '../models/api-client/ApiClient.interface' 4 | import { apiMockClient } from './mock' 5 | import { apiLiveClient } from './live' 6 | 7 | let env: string = 'mock' 8 | if (process.env && process.env.REACT_APP_API_CLIENT) { 9 | env = process.env.REACT_APP_API_CLIENT.trim() 10 | } 11 | // return either the live or the mock client 12 | let apiClient: ApiClientInterface 13 | if (env === 'live') { 14 | apiClient = apiLiveClient 15 | } else { 16 | apiClient = apiMockClient 17 | } 18 | 19 | export { 20 | apiClient 21 | } 22 | -------------------------------------------------------------------------------- /with-create-react-app/src/api-client/live/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/live/index.ts 2 | 3 | import { ApiClientInterface } from '../../models/api-client/ApiClient.interface' 4 | import { itemsApiClient } from './items' 5 | 6 | // create an instance of our main ApiClient that wraps the live child clients 7 | const apiLiveClient: ApiClientInterface = { 8 | items: itemsApiClient 9 | } 10 | // export our instance 11 | export { 12 | apiLiveClient 13 | } 14 | -------------------------------------------------------------------------------- /with-create-react-app/src/api-client/live/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/live/items/index.ts 2 | 3 | import { 4 | ItemsApiClientUrlsInterface, 5 | ItemsApiClientInterface, 6 | ItemsApiClientModel 7 | } from '../../../models/api-client/items' 8 | 9 | const urls: ItemsApiClientUrlsInterface = { 10 | // this should be pointing to your live API end-point 11 | fetchItems: 'https://yourapi-endpoint...' 12 | } 13 | 14 | // instantiate the ItemsApiClient pointing at the url that returns static json mock data 15 | const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel({ urls }) 16 | 17 | // export our instance 18 | export { 19 | itemsApiClient 20 | } 21 | -------------------------------------------------------------------------------- /with-create-react-app/src/api-client/mock/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/mock/index.ts 2 | 3 | import { ApiClientInterface } from '../../models/api-client/ApiClient.interface' 4 | import { itemsApiClient } from './items' 5 | 6 | // create an instance of our main ApiClient that wraps the mock child clients 7 | const apiMockClient: ApiClientInterface = { 8 | items: itemsApiClient 9 | } 10 | 11 | // export our instance 12 | export { 13 | apiMockClient 14 | } 15 | -------------------------------------------------------------------------------- /with-create-react-app/src/api-client/mock/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/api-client/mock/items/index.ts 2 | 3 | import { 4 | ItemsApiClientUrlsInterface, 5 | ItemsApiClientInterface, 6 | ItemsApiClientModel 7 | } from '../../../models/api-client/items' 8 | 9 | const urls: ItemsApiClientUrlsInterface = { 10 | fetchItems: '/static/data/items.json' 11 | } 12 | 13 | // instantiate the ItemsApiClient pointing at the url that returns static json mock data 14 | const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel({ 15 | urls, 16 | mockDelay: 1000 17 | }) 18 | 19 | // export our instance 20 | export { 21 | itemsApiClient 22 | } 23 | -------------------------------------------------------------------------------- /with-create-react-app/src/components/items/ItemsList.component.tsx: -------------------------------------------------------------------------------- 1 | // file: ItemsList.component.tsx 2 | 3 | import React from 'react' 4 | // import reference to our interface 5 | import { ItemInterface } from '../../models/items/Item.interface' 6 | // import reference to your Item component: 7 | import { ItemComponent } from './children/Item.component' 8 | // import a reference to our Loader component: 9 | import { Loader } from '../shared/Loader.component' 10 | 11 | // ItemsList component 12 | export class ItemsListComponent extends React.Component<{ 13 | loading: boolean, 14 | items: ItemInterface[], 15 | onItemSelect: (item: ItemInterface) => void 16 | }> { 17 | constructor(props: { 18 | loading: boolean, 19 | items: ItemInterface[], 20 | onItemSelect: (item: ItemInterface) => void 21 | }) { 22 | super(props) 23 | } 24 | 25 | handleItemClick (item: ItemInterface) { 26 | this.props.onItemSelect(item) 27 | } 28 | 29 | render(): React.ReactNode { 30 | const { loading, items } = this.props 31 | 32 | let element 33 | if (loading) { 34 | // render Loader 35 | element = 36 | } else { 37 | // render
      38 | element =
        39 | { 40 | items.map((item, index) => { 41 | return this.handleItemClick(item)}> 42 | }) 43 | } 44 |
      45 | } 46 | 47 | return
      48 |

      Items:

      49 | {element} 50 |
      51 | } 52 | } 53 | -------------------------------------------------------------------------------- /with-create-react-app/src/components/items/children/Item.behavior.test.tsx: -------------------------------------------------------------------------------- 1 | // file: Item.behavior.test.tsx 2 | 3 | // NOTE: jest-dom adds handy assertions to Jest and it is recommended, but not required. 4 | import '@testing-library/jest-dom' 5 | import { render, fireEvent, prettyDOM } from '@testing-library/react' 6 | 7 | // import reference to our interface 8 | import { ItemInterface } from '../../../models/items/Item.interface' 9 | // import reference to your Item component: 10 | import { ItemComponent } from './Item.component' 11 | 12 | // test our component click event 13 | test('click event invokes onItemSelect handler as expected', () => { 14 | const model: ItemInterface = { 15 | id: 1, 16 | name: 'Unit test item 1', 17 | selected: false 18 | } 19 | 20 | // create a spy function with jest.fn() 21 | const onItemSelect = jest.fn() 22 | 23 | // render our component 24 | const {container} = render() 25 | // get a reference to the
    • element 26 | const liElement = container.firstChild as HTMLElement 27 | // fire click 28 | fireEvent.click(liElement) 29 | // check test result 30 | expect(onItemSelect).toHaveBeenCalledTimes(1) 31 | }) 32 | -------------------------------------------------------------------------------- /with-create-react-app/src/components/items/children/Item.component.tsx: -------------------------------------------------------------------------------- 1 | // file: Item.component.tsx 2 | 3 | import React from 'react' 4 | // import reference to our interface 5 | import { ItemInterface } from '../../../models/items/Item.interface' 6 | 7 | // example using class syntax 8 | export class ItemComponent extends React.Component<{ 9 | model: ItemInterface, 10 | onItemSelect: (item: ItemInterface) => void 11 | }> { 12 | constructor(props: { 13 | model: ItemInterface, 14 | onItemSelect: (item: ItemInterface) => void 15 | }) { 16 | super(props) 17 | } 18 | 19 | get cssClass () { 20 | let css = 'item' 21 | if (this.props.model?.selected) { 22 | css += ' selected' 23 | } 24 | return css.trim() 25 | } 26 | 27 | handleItemClick (item: ItemInterface) { 28 | this.props.onItemSelect(item) 29 | } 30 | 31 | //
      {model.name} [{String(model.selected)}]
      32 | 33 | render(): React.ReactNode { 34 | const { model } = this.props 35 | 36 | return
    • this.handleItemClick(model)}> 37 |
      *
      38 |
      {model.name}
      39 |
    • 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /with-create-react-app/src/components/shared/Loader.component.tsx: -------------------------------------------------------------------------------- 1 | // file: Loader.component.tsx 2 | 3 | import React from 'react' 4 | 5 | // Loader component 6 | export class Loader extends React.Component { 7 | render(): React.ReactNode { 8 | return
      9 |
      10 |
      11 | } 12 | } 13 | -------------------------------------------------------------------------------- /with-create-react-app/src/http-client/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/index.ts 2 | 3 | import { HttpClientInterface } from './models/HttpClient.interface' 4 | import { HttpClientAxios } from './models/HttpClient.axios' 5 | 6 | // export instance of HttpClientInterface 7 | export const httpClient: HttpClientInterface = new HttpClientAxios() 8 | 9 | // also export all our interfaces/models/enums 10 | export * from './models' 11 | -------------------------------------------------------------------------------- /with-create-react-app/src/http-client/models/HttpClient.interface.ts: -------------------------------------------------------------------------------- 1 | // files: src/http-client/models/HttpClient.interface.ts 2 | 3 | import { HttpRequestParamsInterface } from './HttpRequestParams.interface' 4 | 5 | /** 6 | * @name HttpClientInterface 7 | * @description 8 | * Represents our HttpClient. 9 | */ 10 | export interface HttpClientInterface { 11 | /** 12 | * @name request 13 | * @description 14 | * A method that executes different types of http requests (i.e. GET/POST/etc) 15 | * based on the parameters argument. 16 | * The type R specify the type of the result returned 17 | * The type P specify the type of payload if any 18 | * @returns A Promise as the implementation of this method will be async. 19 | */ 20 | request(parameters: HttpRequestParamsInterface

      ): Promise 21 | } 22 | -------------------------------------------------------------------------------- /with-create-react-app/src/http-client/models/HttpRequestParams.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/HttpRequestParams.interface.ts 2 | 3 | import { HttpRequestType } from './HttpRequestType.enum' 4 | 5 | /** 6 | * @name HttpRequestParamsInterface 7 | * @description 8 | * Interface represents an object we'll use to pass arguments into our HttpClient request method. 9 | * This allow us to specify the type of request we want to execute, the end-point url, 10 | * if the request should include an authentication token, and an optional payload (if POST or PUT for example) 11 | */ 12 | export interface HttpRequestParamsInterface

      { 13 | requestType: HttpRequestType 14 | url: string 15 | requiresToken: boolean 16 | payload?: P 17 | } 18 | -------------------------------------------------------------------------------- /with-create-react-app/src/http-client/models/HttpRequestType.enum.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/HttpRequestType.ts 2 | 3 | /** 4 | * @name HttpRequestType 5 | * @description 6 | * The type of http request we need to execute in our HttpClient request method 7 | */ 8 | export const enum HttpRequestType { 9 | get, 10 | post, 11 | put, 12 | delete 13 | } 14 | -------------------------------------------------------------------------------- /with-create-react-app/src/http-client/models/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/http-client/models/index.ts 2 | 3 | export * from './HttpRequestType.enum' 4 | export * from './HttpRequestParams.interface' 5 | export * from './HttpClient.interface' 6 | export * from './HttpClient.axios' 7 | -------------------------------------------------------------------------------- /with-create-react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /with-create-react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | // file: index.tsx 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import './index.css'; 6 | import App from './App'; 7 | import reportWebVitals from './reportWebVitals'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /with-create-react-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /with-create-react-app/src/models/api-client/ApiClient.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/api-client/ApiClient.interface.ts 2 | 3 | import { ItemsApiClientInterface } from './items' 4 | 5 | /** 6 | * @Name ApiClientInterface 7 | * @description 8 | * Interface wraps all api client modules into one places for keeping code organized. 9 | */ 10 | export interface ApiClientInterface { 11 | items: ItemsApiClientInterface 12 | } 13 | -------------------------------------------------------------------------------- /with-create-react-app/src/models/api-client/items/ItemsApiClient.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/api-client/items/ItemsApiClient.interface.ts 2 | 3 | import { ItemInterface } from '../../items/Item.interface' 4 | 5 | /** 6 | * @Name ItemsApiClientInterface 7 | * @description 8 | * Interface for the Items api client module 9 | */ 10 | export interface ItemsApiClientInterface { 11 | fetchItems: () => Promise 12 | } 13 | -------------------------------------------------------------------------------- /with-create-react-app/src/models/api-client/items/ItemsApiClient.model.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/api-client/items/ItemsApiClient.model.ts 2 | 3 | import { httpClient, HttpRequestParamsInterface, HttpRequestType } from '../../../http-client' 4 | 5 | import { ItemsApiClientUrlsInterface } from './ItemsApiClientUrls.interface' 6 | import { ItemsApiClientInterface } from './ItemsApiClient.interface' 7 | import { ItemInterface } from '../../items/Item.interface' 8 | 9 | /** 10 | * @Name ItemsApiClientModel 11 | * @description 12 | * Implements the ItemsApiClientInterface interface 13 | */ 14 | export class ItemsApiClientModel implements ItemsApiClientInterface { 15 | private readonly urls!: ItemsApiClientUrlsInterface 16 | private readonly mockDelay: number = 0 17 | 18 | constructor(options: { 19 | urls: ItemsApiClientUrlsInterface, 20 | mockDelay?: number 21 | }) { 22 | this.urls = options.urls 23 | if (options.mockDelay) { 24 | this.mockDelay = options.mockDelay 25 | } 26 | } 27 | 28 | fetchItems(): Promise { 29 | const requestParameters: HttpRequestParamsInterface = { 30 | requestType: HttpRequestType.get, 31 | url: this.urls.fetchItems, 32 | requiresToken: false, 33 | } 34 | 35 | //return httpClient.request(requestParameters) 36 | 37 | // if you want to keep simulating the artificail delay, use this 38 | if (!this.mockDelay) { 39 | return httpClient.request(requestParameters) 40 | } else { 41 | return new Promise((resolve) => { 42 | httpClient.request(requestParameters) 43 | .then((data) => { 44 | setTimeout(() => { 45 | resolve(data) 46 | }, this.mockDelay) 47 | }) 48 | }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /with-create-react-app/src/models/api-client/items/ItemsApiClientUrls.interface.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/api-client/items/ItemsApiClientUrls.interface.ts 2 | 3 | /** 4 | * @Name ItemsApiClientUrlsInterface 5 | * @description 6 | * Interface for the Items urls used to avoid hard-coded strings 7 | */ 8 | export interface ItemsApiClientUrlsInterface { 9 | fetchItems: string 10 | } 11 | -------------------------------------------------------------------------------- /with-create-react-app/src/models/api-client/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/models/api-client/items/index.ts 2 | 3 | export * from './ItemsApiClientUrls.interface' 4 | export * from './ItemsApiClient.interface' 5 | export * from './ItemsApiClient.model' 6 | -------------------------------------------------------------------------------- /with-create-react-app/src/models/items/Item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ItemInterface { 2 | id: number 3 | name: string 4 | selected: boolean 5 | } 6 | -------------------------------------------------------------------------------- /with-create-react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /with-create-react-app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /with-create-react-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /with-create-react-app/src/shims-react.d.ts: -------------------------------------------------------------------------------- 1 | // file: src/shims-react.d.ts 2 | 3 | declare interface process { 4 | env: { 5 | REACT_APP_API_CLIENT: string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/index.ts 2 | 3 | export * from './root' 4 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/items/Items.slice.ts: -------------------------------------------------------------------------------- 1 | // src/store/items/Items.slice.ts 2 | 3 | // import createSlice and PayloadAction from redux toolkit 4 | import { createSlice, PayloadAction } from '@reduxjs/toolkit' 5 | 6 | // import out items state interface, and the item interface 7 | import { ItemsStateInterface } from './models' 8 | import { ItemInterface } from '../../models/items/Item.interface' 9 | 10 | // create an object that represents our initial items state 11 | const initialItemsState: ItemsStateInterface = { 12 | loading: false, 13 | items: [] 14 | } 15 | 16 | // create the itemsStoreSlice with createSlice: 17 | export const itemsStoreSlice = createSlice({ 18 | name: 'itemsStoreSlice', 19 | initialState: initialItemsState, 20 | reducers: { 21 | // reducers are functions that commit final mutations to the state 22 | // These will commit final mutation/changes to the state 23 | 24 | setLoading: (state, action: PayloadAction) => { 25 | state.loading = action.payload 26 | }, 27 | 28 | setItems: (state, action: PayloadAction) => { 29 | // update our state: 30 | // set our items 31 | state.items = action.payload || [] 32 | // set loading to false so the loader will be hidden in the UI 33 | state.loading = false 34 | }, 35 | 36 | setItemSelected: (state, action: PayloadAction) => { 37 | const item = action.payload 38 | const found = state.items.find(o => o.id === item.id) as ItemInterface 39 | found.selected = !found.selected 40 | } 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/items/Items.store.ts: -------------------------------------------------------------------------------- 1 | // src/store/items/Items.store.ts 2 | 3 | // import hooks useSelector and useDispatch from react-redux 4 | import { useSelector, useDispatch } from 'react-redux' 5 | 6 | // import a refence to our itemsStoreSlice 7 | import { itemsStoreSlice } from './Items.slice' 8 | // import a reference to our RootStateInterface 9 | import { RootStateInterface } from '../root' 10 | // import references to our itesms tore and actions interfaces 11 | import { ItemsStoreInterface, ItemsStoreActionsInterface } from './models' 12 | // import a reference to our ItemInterface 13 | import { ItemInterface } from '../../models/items/Item.interface' 14 | 15 | // import a reference to our apiClient instance 16 | import { apiClient } from '../../api-client' 17 | 18 | // hook to allows us to consume the ItemsStore instance in our components 19 | export function useItemsStore(): ItemsStoreInterface { 20 | // note: we are callin dispatch "commit" here, as it make more sense to call it this way 21 | // feel free to just call it dispatch if you prefer 22 | const commit = useDispatch() 23 | 24 | // get a reference to our slice actions (which are really our mutations/commits) 25 | const mutations = itemsStoreSlice.actions 26 | 27 | // our items store actions implementation: 28 | const actions: ItemsStoreActionsInterface = { 29 | loadItems: async () => { 30 | commit(mutations.setLoading(true)) 31 | 32 | // invoke our API cient fetchItems to load the data from an API end-point 33 | const data = await apiClient.items.fetchItems() 34 | commit(mutations.setItems(data)) 35 | }, 36 | toggleItemSelected: async (item: ItemInterface) => { 37 | console.log('ItemsStore: action: toggleItemSelected', item) 38 | commit(mutations.setItemSelected(item)) 39 | } 40 | } 41 | 42 | // return our store intance implementation 43 | return { 44 | getters: { 45 | loading: useSelector((s: RootStateInterface) => s.items.loading), 46 | items: useSelector((s: RootStateInterface) => s.items.items) 47 | }, 48 | actions: actions 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/items/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/items/index.ts 2 | 3 | export * from './Items.slice' 4 | export * from './Items.store' 5 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/items/models/ItemsState.interface.ts: -------------------------------------------------------------------------------- 1 | // file: ItemsState.interface.ts 2 | 3 | import { ItemInterface } from '../../../models/items/Item.interface' 4 | 5 | /** 6 | * @name ItemsStateInterface 7 | * @description Interface represnets our Items state 8 | */ 9 | export interface ItemsStateInterface { 10 | loading: boolean 11 | items: ItemInterface[] 12 | } 13 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/items/models/ItemsStore.interface.ts: -------------------------------------------------------------------------------- 1 | // file: ItemsStore.interface.ts 2 | 3 | import { ItemInterface } from '../../../models/items/Item.interface' 4 | 5 | /** 6 | * @name ItemsStoreActionsInterface 7 | * @description Interface represents our Items state actions 8 | */ 9 | export interface ItemsStoreActionsInterface { 10 | loadItems (): Promise 11 | toggleItemSelected (item: ItemInterface): Promise 12 | } 13 | 14 | /** 15 | * @name ItemsStoreGettersInterface 16 | * @description Interface represents our store getters 17 | * Getters will be used to consume the data from the store. 18 | */ 19 | export interface ItemsStoreGettersInterface { 20 | loading: boolean 21 | items: ItemInterface[] 22 | } 23 | 24 | /** 25 | * @name ItemsStoreInterface 26 | * @description Interface represents our Items store module 27 | */ 28 | export interface ItemsStoreInterface { 29 | actions: ItemsStoreActionsInterface 30 | getters: ItemsStoreGettersInterface 31 | } 32 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/items/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ItemsState.interface' 2 | export * from './ItemsStore.interface' 3 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/root/Root.store.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/Root.store.ts 2 | 3 | // import configureStore from redux toolkit 4 | import { configureStore } from '@reduxjs/toolkit' 5 | 6 | // import our root store interface 7 | import { RootStoreInterface } from './models' 8 | 9 | // import our items slice and store 10 | import { itemsStoreSlice, useItemsStore } from '../items/' 11 | 12 | // configure global redux store for the whole app. 13 | // this will be consumed by App.tsx 14 | export const globalStore = configureStore({ 15 | reducer: { 16 | //add reducers here 17 | items: itemsStoreSlice.reducer 18 | } 19 | }) 20 | 21 | // Infer the `RootStateInterface` type from the store itself (globalStore.getState) 22 | // thus avoiding to explicitely having to create an additional interface for the 23 | export type RootStateInterface = ReturnType 24 | // Infer the dispatch type from globalStore.dispatch itself 25 | //export type AppDispatch = typeof globalStore.dispatch 26 | 27 | // hook that returns our root store instance and will allow us to consume our app store from our components 28 | export function useAppStore(): RootStoreInterface { 29 | return { 30 | itemsStore: useItemsStore(), 31 | // additional domain store modules will be eventually added here 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/root/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/index.ts 2 | 3 | export * from './Root.store' 4 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/root/models/RootStore.interface.ts: -------------------------------------------------------------------------------- 1 | // file: RootStore.interface.ts 2 | 3 | import { ItemsStoreInterface } from '../../items/models/ItemsStore.interface' 4 | 5 | /** 6 | * @name RootStateInterface 7 | * @description Interface represents our global state manager 8 | */ 9 | export interface RootStoreInterface { 10 | itemsStore: ItemsStoreInterface, 11 | // additional domain store modules will be eventually added here 12 | } 13 | -------------------------------------------------------------------------------- /with-create-react-app/src/store/root/models/index.ts: -------------------------------------------------------------------------------- 1 | // file: src/store/root/models/index.ts 2 | 3 | export * from './RootStore.interface' 4 | -------------------------------------------------------------------------------- /with-create-react-app/src/tests/http-client/HttpClient.request.get.test.ts: -------------------------------------------------------------------------------- 1 | // file: src/tests/http-client/HttpClient.request.get.test.ts 2 | 3 | import axios from 'axios' 4 | import { httpClient, HttpRequestType, HttpRequestParamsInterface } from '../../http-client' 5 | 6 | 7 | let mockRequestParams: HttpRequestParamsInterface = { 8 | requestType: HttpRequestType.get, 9 | url: 'path/to/a/get/api/endpoint', 10 | requiresToken: false 11 | } 12 | 13 | // test our component click event 14 | test('httpClient: reqest: should execute get request succesfully', () => { 15 | jest 16 | .spyOn(axios, 'get') 17 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${ mockRequestParams.url }`})); 18 | 19 | httpClient 20 | .request(mockRequestParams) 21 | .then((response) => { 22 | //console.debug('response:', response) 23 | expect(response).toEqual(`request completed: ${ mockRequestParams.url }`) 24 | }) 25 | .catch(error => { 26 | console.info('HttpClient.request.get.test.ts: HttpClient.request(get) error', error) 27 | }) 28 | }) 29 | 30 | test('httpClient: reqest: get should throw error on rejection', () => { 31 | jest 32 | .spyOn(axios, 'get') 33 | .mockImplementation(async () => Promise.reject({ data: `request completed: ${ mockRequestParams.url }`})); 34 | 35 | httpClient 36 | .request(mockRequestParams) 37 | .catch(error => { 38 | expect(error).toBeDefined() 39 | expect(error.toString()).toEqual('Error: HttpClient exception') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /with-create-react-app/src/tests/http-client/HttpClient.request.post.test.ts: -------------------------------------------------------------------------------- 1 | // file: src/tests/http-client/HttpClient.request.post.test.ts 2 | 3 | import axios from 'axios' 4 | import { httpClient, HttpRequestType, HttpRequestParamsInterface } from '../../http-client' 5 | 6 | let mockRequestParams: HttpRequestParamsInterface = { 7 | requestType: HttpRequestType.post, 8 | url: 'path/to/a/post/api/endpoint', 9 | requiresToken: false, 10 | payload: {} 11 | } 12 | 13 | type P = typeof mockRequestParams.payload 14 | 15 | // test our component click event 16 | test('httpClient: reqest: should execute post request succesfully', () => { 17 | jest 18 | .spyOn(axios, 'post') 19 | .mockImplementation(async () => Promise.resolve({ data: `request completed: ${ mockRequestParams.url }`})); 20 | 21 | httpClient 22 | .request(mockRequestParams) 23 | .then((response) => { 24 | //console.debug('response:', response) 25 | expect(response).toEqual(`request completed: ${ mockRequestParams.url }`) 26 | }) 27 | .catch(error => { 28 | console.info('HttpClient.request.post.test.ts: HttpClient.request(post) error', error) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /with-create-react-app/src/views/Items.view.tsx: -------------------------------------------------------------------------------- 1 | // file: src/views/Items.view.tsx 2 | 3 | // import hook useEffect from react 4 | import { useEffect } from 'react' 5 | // import a reference to our ItemInterface 6 | import { ItemInterface } from '../models/items/Item.interface' 7 | // import a reference to your ItemsList component: 8 | import { ItemsListComponent } from '../components/items/ItemsList.component' 9 | // import our useAppStore hook from our store 10 | import { useAppStore } from '../store' 11 | 12 | // ItemsView component: 13 | function ItemsView() { 14 | // get a reference to our itemsStore instanceusing our useAppStore() hook: 15 | const { 16 | itemsStore 17 | } = useAppStore() 18 | 19 | // get a reference to the items state data through our itemsStore getters: 20 | const { 21 | loading, 22 | items 23 | } = itemsStore.getters 24 | 25 | // item select event handler 26 | const onItemSelect = (item: ItemInterface) => { 27 | console.log('ItemsView: onItemSelect', item) 28 | itemsStore.actions.toggleItemSelected(item) 29 | } 30 | 31 | // use React useEffect to invoke our itemsStore loadItems action only once after this component is rendered: 32 | useEffect(() => { 33 | itemsStore.actions.loadItems() 34 | }, []); // <-- empty array means 'run once' 35 | 36 | // return our render function containing our ItemslistComponent as we did earlier in the App.tsx file 37 | return ( 38 |

      39 | 40 |
      41 | ) 42 | } 43 | 44 | export default ItemsView 45 | -------------------------------------------------------------------------------- /with-create-react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------