├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── package.json ├── packages ├── core │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── cart │ │ │ ├── data │ │ │ │ ├── CartInMemoryRepository.ts │ │ │ │ └── index.ts │ │ │ ├── domain │ │ │ │ ├── Cart.ts │ │ │ │ ├── CartItem.ts │ │ │ │ ├── CartRepository.ts │ │ │ │ ├── __test__ │ │ │ │ │ └── Cart.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── usecases │ │ │ │ │ ├── AddProductToCartUseCase.ts │ │ │ │ │ ├── EditQuantityOfCartItemUseCase.ts │ │ │ │ │ ├── GetCartUseCase.ts │ │ │ │ │ ├── RemoveItemFromCartUseCase.ts │ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── presentation │ │ │ │ ├── CartPloc.ts │ │ │ │ ├── CartState.ts │ │ │ │ └── index.ts │ │ ├── common │ │ │ ├── dependencies │ │ │ │ ├── DependenciesLocator.ts │ │ │ │ └── index.ts │ │ │ ├── domain │ │ │ │ ├── DataError.ts │ │ │ │ ├── Either.ts │ │ │ │ └── EitherAsync.ts │ │ │ ├── index.ts │ │ │ └── presentation │ │ │ │ ├── Ploc.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── products │ │ │ ├── data │ │ │ ├── ProductInMemoryRepository.ts │ │ │ └── index.ts │ │ │ ├── domain │ │ │ ├── GetProductsUseCase.ts │ │ │ ├── Product.ts │ │ │ ├── ProductRepository.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── presentation │ │ │ ├── ProductsPloc.ts │ │ │ ├── ProductsState.ts │ │ │ └── index.ts │ └── tsconfig.json ├── react-app │ ├── .eslintcache │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── app │ │ │ ├── App.tsx │ │ │ └── __test__ │ │ │ │ └── App.test.tsx │ │ ├── appbar │ │ │ ├── Logo.png │ │ │ ├── MyAppBar.tsx │ │ │ └── react-logo.png │ │ ├── cart │ │ │ ├── CartContent.tsx │ │ │ ├── CartContentItem.tsx │ │ │ └── CartDrawer.tsx │ │ ├── common │ │ │ ├── Context.tsx │ │ │ └── usePlocState.tsx │ │ ├── index.tsx │ │ ├── products │ │ │ ├── ProductItem.tsx │ │ │ └── ProductList.tsx │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── theme.tsx │ ├── tsconfig.json │ └── yarn.lock └── vue-app │ ├── .browserslistrc │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── appbar │ │ └── MyAppBar.vue │ ├── assets │ │ ├── logo.png │ │ └── vue-logo.png │ ├── cart │ │ ├── CartContent.vue │ │ ├── CartContenttItem.vue │ │ └── CartSidebar.vue │ ├── common │ │ └── usePlocState.ts │ ├── main.ts │ ├── products │ │ ├── ProductItem.vue │ │ └── ProductList.vue │ └── shims-vue.d.ts │ ├── tsconfig.json │ └── yarn.lock ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "plugin:@typescript-eslint/recommended", 4 | "plugin:prettier/recommended", 5 | "prettier/@typescript-eslint", 6 | ], 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: "module", 10 | }, 11 | rules: { 12 | "no-console": "error", 13 | "@typescript-eslint/explicit-function-return-type": ["off"], 14 | "@typescript-eslint/explicit-module-boundary-types": ["off"], 15 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 16 | 17 | "no-unused-expressions": "off", 18 | "no-useless-concat": "off", 19 | "no-useless-constructor": "off", 20 | "default-case": "off", 21 | "@typescript-eslint/no-use-before-define": "off", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/no-empty-interface": "off", 24 | "@typescript-eslint/ban-ts-ignore": "off", 25 | "@typescript-eslint/no-empty-function": "off", 26 | }, 27 | settings: { 28 | react: { 29 | version: "detect", 30 | }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .eslintcache 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | module.exports = { 4 | printWidth: 100, 5 | tabWidth: 4, 6 | useTabs: false, 7 | semi: true, 8 | singleQuote: false, 9 | trailingComma: "es5", 10 | bracketSpacing: true, 11 | jsxBracketSameLine: true, 12 | arrowParens: "avoid", 13 | rangeStart: 0, 14 | rangeEnd: Infinity, 15 | proseWrap: "preserve", 16 | requirePragma: false, 17 | insertPragma: false, 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jorge Sánchez Fernández 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 | ![frontend-clean-architecture](https://user-images.githubusercontent.com/5593590/118298368-aa221880-b4df-11eb-96d7-e72bfbecee62.png) 2 | 3 | 4 | The purpose of this repository is to follow the principles of Clean Architecture in frontend using different frameworks such as ReactJS and VueJs 5 | 6 | ## Clean Architecture Course 7 | 8 | * [Curso Clean Architecture](https://xurxodev.com/curso-clean-architecture) 9 | 10 | ## Blog post with detail explanation 11 | 12 | * [Moving away from ReactJs and VueJs on front-end using Clean Architecture](https://xurxodev.medium.com/frontend-clean-architecture-ca2592bd9d58) (English) 13 | * [Alejándonos de ReactJs y VueJs en el frontend usando Clean Architecture](https://xurxodev.com/frontend-clean_architecture) (Spanish) 14 | 15 | ## Screenshots 16 | 17 | ![shopping-cart-react-vue](https://user-images.githubusercontent.com/5593590/118297753-ddb07300-b4de-11eb-8ec9-23c3a14d1883.png) 18 | 19 | 20 | ## Clean Architecture 21 | 22 | This app use Clean architecture 23 | 24 | ![bloc-clean-architecture](https://user-images.githubusercontent.com/5593590/82728951-03ec6a00-9cf4-11ea-8557-011a3dea7804.png) 25 | 26 | ## BloC Pattern 27 | 28 | This app use Bloc pattern as presentation pattern 29 | 30 | ![bloc-unidirectional-data-flow](https://user-images.githubusercontent.com/5593590/118929889-6008bf00-b945-11eb-865a-1ca3df8d618e.png) 31 | 32 | 33 | ## Available Scripts 34 | 35 | In the project directory, you can run: 36 | 37 | ### Development 38 | 39 | Start react development server: 40 | ``` 41 | $ yarn react start 42 | ``` 43 | 44 | Start vue development server: 45 | ``` 46 | $ yarn vue serve 47 | ``` 48 | 49 | ### Testing 50 | 51 | Run unit tests: 52 | 53 | ``` 54 | $ yarn test 55 | ``` 56 | 57 | ## License 58 | 59 | MIT License 60 | 61 | Copyright (c) 2021 Jorge Sánchez Fernández 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining a copy 64 | of this software and associated documentation files (the "Software"), to deal 65 | in the Software without restriction, including without limitation the rights 66 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 67 | copies of the Software, and to permit persons to whom the Software is 68 | furnished to do so, subject to the following conditions: 69 | 70 | The above copyright notice and this permission notice shall be included in all 71 | copies or substantial portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 76 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 78 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 79 | SOFTWARE. 80 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "frontend-clean-architecture", 4 | "version": "0.1.0", 5 | "description": "The purpose of this repository is to follow the principles of Clean Architecture in frontend using different frameworks such as ReactJS and VueJs.", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "core": "yarn workspace @frontend-clean-architecture/core", 11 | "react": "yarn workspace @frontend-clean-architecture/react-app", 12 | "vue": "yarn workspace @frontend-clean-architecture/vue-app", 13 | "prettify": "prettier \"packages/**/src/**/*.{js,jsx,json,css,ts,tsx,vue}\" --write", 14 | "lint": "eslint packages/*/src/ --ext ts,tsx,vue", 15 | "test": "yarn core test && yarn react test" 16 | }, 17 | "devDependencies": { 18 | "@typescript-eslint/eslint-plugin": "^4.9.1", 19 | "@typescript-eslint/parser": "^4.9.1", 20 | "babel-loader": "8.1.0", 21 | "eslint": "^7.15.0", 22 | "eslint-config-prettier": "^7.0.0", 23 | "eslint-plugin-flowtype": "^5.2.0", 24 | "eslint-plugin-import": "^2.22.1", 25 | "eslint-plugin-jsx-a11y": "^6.4.1", 26 | "eslint-plugin-prettier": "^3.2.0", 27 | "prettier": "^2.2.1", 28 | "typescript": "^4.1.2" 29 | } 30 | } -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | build 11 | 12 | tsconfig.tsbuildinfo 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jorge Sánchez Fernández 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 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # core 2 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/t2/0czw19k57dl36ks13766_ghc0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: null, 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: "node", 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | transform: { 171 | "^.+\\.ts?$": "ts-jest", 172 | }, 173 | 174 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 175 | // transformIgnorePatterns: [ 176 | // "/node_modules/" 177 | // ], 178 | 179 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 180 | // unmockedModulePathPatterns: undefined, 181 | 182 | // Indicates whether each individual test should be reported during the run 183 | // verbose: null, 184 | 185 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 186 | // watchPathIgnorePatterns: [], 187 | 188 | // Whether to use watchman for file crawling 189 | // watchman: true, 190 | }; 191 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-clean-architecture/core", 3 | "version": "0.1.0", 4 | "description": "Frontend Clean Architecture Core, this project contains all reusable entities and logic.", 5 | "main": "build/index.js", 6 | "repository": "https://github.com/xurxodev/karate-stars-api", 7 | "author": "Jorge Sánchez ", 8 | "scripts": { 9 | "build": "tsc --build", 10 | "start": "tsc --watch", 11 | "test": "jest" 12 | }, 13 | "dependencies": {}, 14 | "devDependencies": { 15 | "@types/jest": "26.0.19", 16 | "jest": "26.6.0", 17 | "ts-jest": "26.4.4" 18 | } 19 | } -------------------------------------------------------------------------------- /packages/core/src/cart/data/CartInMemoryRepository.ts: -------------------------------------------------------------------------------- 1 | import { CartRepository } from "../domain/CartRepository"; 2 | import { Cart } from "../domain/Cart"; 3 | import { DataError } from "../../common/domain/DataError"; 4 | import { Either } from "../../common/domain/Either"; 5 | 6 | export class CartInMemoryRepository implements CartRepository { 7 | cart = new Cart([ 8 | { 9 | id: "1", 10 | image: "https://images-na.ssl-images-amazon.com/images/I/71Y1S1m-QAL._AC_UY879_.jpg", 11 | title: "Element Blazin LS tee Shirt, Hombre", 12 | price: 19.95, 13 | quantity: 3, 14 | }, 15 | { 16 | id: "2", 17 | image: "https://m.media-amazon.com/images/I/81HnHYik58L._AC_UL640_FMwebp_QL65_.jpg", 18 | title: "Element Vertical SS tee Shirt, Hombre", 19 | price: 21.95, 20 | quantity: 1, 21 | }, 22 | ]); 23 | 24 | get(): Promise> { 25 | return new Promise((resolve, _reject) => { 26 | setTimeout(() => { 27 | try { 28 | resolve(Either.right(this.cart)); 29 | } catch (error) { 30 | resolve(Either.left({ kind: "UnexpectedError", error })); 31 | } 32 | }, 100); 33 | }); 34 | } 35 | 36 | save(cart: Cart): Promise> { 37 | return new Promise((resolve, _reject) => { 38 | setTimeout(() => { 39 | try { 40 | this.cart = cart; 41 | resolve(Either.right(true)); 42 | } catch (error) { 43 | resolve(Either.left({ kind: "UnexpectedError", error })); 44 | } 45 | }, 100); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/cart/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CartInMemoryRepository"; 2 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/Cart.ts: -------------------------------------------------------------------------------- 1 | import { CartItem } from "./CartItem"; 2 | 3 | type TotalPrice = number; 4 | type TotalItems = number; 5 | 6 | export class Cart { 7 | items: readonly CartItem[]; 8 | readonly totalPrice: TotalPrice; 9 | readonly totalItems: TotalItems; 10 | 11 | constructor(items: CartItem[]) { 12 | this.items = items; 13 | this.totalPrice = this.calculateTotalPrice(items); 14 | this.totalItems = this.calculateTotalItems(items); 15 | } 16 | 17 | static createEmpty(): Cart { 18 | return new Cart([]); 19 | } 20 | 21 | addItem(item: CartItem): Cart { 22 | const existedItem = this.items.find(i => i.id === item.id); 23 | 24 | if (existedItem) { 25 | const newItems = this.items.map(oldItem => { 26 | if (oldItem.id === item.id) { 27 | return { ...oldItem, quantity: oldItem.quantity + item.quantity }; 28 | } else { 29 | return oldItem; 30 | } 31 | }); 32 | 33 | return new Cart(newItems); 34 | } else { 35 | const newItems = [...this.items, item]; 36 | 37 | return new Cart(newItems); 38 | } 39 | } 40 | 41 | removeItem(itemId: string): Cart { 42 | const newItems = this.items.filter(i => i.id !== itemId); 43 | 44 | return new Cart(newItems); 45 | } 46 | 47 | editItem(itemId: string, quantity: number): Cart { 48 | const newItems = this.items.map(oldItem => { 49 | if (oldItem.id === itemId) { 50 | return { ...oldItem, quantity: quantity }; 51 | } else { 52 | return oldItem; 53 | } 54 | }); 55 | 56 | return new Cart(newItems); 57 | } 58 | 59 | private calculateTotalPrice(items: CartItem[]): TotalPrice { 60 | return +items 61 | .reduce((accumulator, item) => accumulator + item.quantity * item.price, 0) 62 | .toFixed(2); 63 | } 64 | 65 | private calculateTotalItems(items: CartItem[]): TotalItems { 66 | return +items.reduce((accumulator, item) => accumulator + item.quantity, 0); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/CartItem.ts: -------------------------------------------------------------------------------- 1 | import { Price, Title, Image } from "../../products/domain/Product"; 2 | 3 | type CartItemId = string; 4 | type Quantity = number; 5 | 6 | export interface CartItem { 7 | readonly id: CartItemId; 8 | readonly image: Image; 9 | readonly title: Title; 10 | readonly price: Price; 11 | readonly quantity: Quantity; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/CartRepository.ts: -------------------------------------------------------------------------------- 1 | import { DataError } from "../../common/domain/DataError"; 2 | import { Either } from "../../common/domain/Either"; 3 | import { Cart } from "./Cart"; 4 | 5 | export interface CartRepository { 6 | get(): Promise>; 7 | save(cart: Cart): Promise>; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/__test__/Cart.test.ts: -------------------------------------------------------------------------------- 1 | import { Cart } from "../Cart"; 2 | import { CartItem } from "../CartItem"; 3 | 4 | describe("shopping cart", () => { 5 | describe("constructor", () => { 6 | it("should return totalPrice 0 and empty items if shopping cart is created using constructor with empty items", () => { 7 | const shoppingCart = new Cart([]); 8 | 9 | expect(shoppingCart.items).toEqual([]); 10 | expect(shoppingCart.totalPrice).toEqual(0); 11 | expect(shoppingCart.totalItems).toEqual(0); 12 | }); 13 | 14 | it("should return totalPrice equal to item price and item if shopping cart is created using constructor with 1 item", () => { 15 | const items = [givenAShoppingCartItem(1, 29.99)]; 16 | const shoppingCart = new Cart(items); 17 | 18 | expect(shoppingCart.items).toEqual(items); 19 | expect(shoppingCart.totalPrice).toEqual(29.99); 20 | expect(shoppingCart.totalItems).toEqual(1); 21 | }); 22 | 23 | it("should return expected totalPrice and items if shopping cart is created using constructor with 2 items with quantity = 1", () => { 24 | const items = [givenAShoppingCartItem(1, 29.99), givenAShoppingCartItem(1, 39.94)]; 25 | const shoppingCart = new Cart(items); 26 | 27 | expect(shoppingCart.items).toEqual(items); 28 | expect(shoppingCart.totalPrice).toEqual(69.93); 29 | expect(shoppingCart.totalItems).toEqual(2); 30 | }); 31 | 32 | it("should return expected totalPrice and items if shopping cart is created using constructor with 2 items witn quantity > 1", () => { 33 | const items = [givenAShoppingCartItem(2, 29.99), givenAShoppingCartItem(5, 39.94)]; 34 | const shoppingCart = new Cart(items); 35 | 36 | expect(shoppingCart.items).toEqual(items); 37 | expect(shoppingCart.totalPrice).toEqual(259.68); 38 | expect(shoppingCart.totalItems).toEqual(7); 39 | }); 40 | }); 41 | 42 | describe("createEmpty", () => { 43 | it("should return totalPrice 0 and empty items if shopping cart is created using create empty", () => { 44 | const shoppingCart = Cart.createEmpty(); 45 | 46 | expect(shoppingCart.items).toEqual([]); 47 | expect(shoppingCart.totalPrice).toEqual(0); 48 | expect(shoppingCart.totalItems).toEqual(0); 49 | }); 50 | }); 51 | 52 | describe("addItem", () => { 53 | it("should return expected totalPrice and items if item with quantity 1 is added", () => { 54 | const items = [givenAShoppingCartItem(1, 29.99)]; 55 | const shoppingCart = new Cart(items); 56 | const newShoppingCart = shoppingCart.addItem(givenAShoppingCartItem(1, 39.94)); 57 | 58 | expect(newShoppingCart.items).toHaveLength(2); 59 | expect(newShoppingCart.totalPrice).toEqual(69.93); 60 | expect(newShoppingCart.totalItems).toEqual(2); 61 | }); 62 | 63 | it("should return expected totalPrice and items if item with quantity > 1 is added", () => { 64 | const items = [givenAShoppingCartItem(1, 29.99)]; 65 | const shoppingCart = new Cart(items); 66 | const newShoppingCart = shoppingCart.addItem(givenAShoppingCartItem(3, 39.94)); 67 | 68 | expect(newShoppingCart.items).toHaveLength(2); 69 | expect(newShoppingCart.totalPrice).toEqual(149.81); 70 | expect(newShoppingCart.totalItems).toEqual(4); 71 | }); 72 | 73 | it("should increment quantity to existed item and totalPrice if add a existed item again", () => { 74 | const items = [givenAShoppingCartItem(1, 29.99)]; 75 | const shoppingCart = new Cart(items); 76 | const newShoppingCart = shoppingCart.addItem(items[0]); 77 | 78 | expect(newShoppingCart.items).toHaveLength(1); 79 | expect(newShoppingCart.totalPrice).toEqual(59.98); 80 | expect(newShoppingCart.totalItems).toEqual(2); 81 | }); 82 | }); 83 | 84 | describe("removeItem", () => { 85 | it("should return totalPrice 0 and empty items if remove unique item", () => { 86 | const items = [givenAShoppingCartItem(1, 29.99)]; 87 | const shoppingCart = new Cart(items); 88 | const newShoppingCart = shoppingCart.removeItem(items[0].id); 89 | 90 | expect(newShoppingCart.items).toEqual([]); 91 | expect(newShoppingCart.totalPrice).toEqual(0); 92 | expect(newShoppingCart.totalItems).toEqual(0); 93 | }); 94 | 95 | it("should return expected totalPrice and items if remove item", () => { 96 | const items = [givenAShoppingCartItem(1, 29.99), givenAShoppingCartItem(5, 39.94)]; 97 | const shoppingCart = new Cart(items); 98 | const newShoppingCart = shoppingCart.removeItem(items[1].id); 99 | 100 | expect(newShoppingCart.items).toHaveLength(1); 101 | expect(newShoppingCart.totalPrice).toEqual(29.99); 102 | expect(newShoppingCart.totalItems).toEqual(1); 103 | }); 104 | }); 105 | 106 | describe("editItem", () => { 107 | it("should return expected totalPrice and items if edit quantity to unique item", () => { 108 | const items = [givenAShoppingCartItem(1, 29.99)]; 109 | const shoppingCart = new Cart(items); 110 | 111 | const newShoppingCart = shoppingCart.editItem(items[0].id, 2); 112 | 113 | expect(newShoppingCart.items).toHaveLength(1); 114 | expect(newShoppingCart.totalPrice).toEqual(59.98); 115 | expect(newShoppingCart.totalItems).toEqual(2); 116 | }); 117 | 118 | it("should return expected totalPrice and items if edit quantity to a item", () => { 119 | const items = [givenAShoppingCartItem(1, 29.99), givenAShoppingCartItem(5, 39.94)]; 120 | const shoppingCart = new Cart(items); 121 | 122 | const newShoppingCart = shoppingCart.editItem(items[0].id, 2); 123 | 124 | expect(newShoppingCart.items).toHaveLength(2); 125 | expect(newShoppingCart.totalPrice).toEqual(259.68); 126 | expect(newShoppingCart.totalItems).toEqual(7); 127 | }); 128 | }); 129 | }); 130 | 131 | function givenAShoppingCartItem(quantity = 1, price = 0): CartItem { 132 | return { 133 | id: Math.random().toString(36).substr(2, 9), 134 | image: "Fake image", 135 | title: "Fake title", 136 | price: price, 137 | quantity: quantity, 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./usecases"; 2 | export * from "./CartRepository"; 3 | export * from "./Cart"; 4 | export * from "./CartItem"; 5 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/usecases/AddProductToCartUseCase.ts: -------------------------------------------------------------------------------- 1 | import { CartRepository } from "../CartRepository"; 2 | import { Cart } from "../Cart"; 3 | import { Product } from "../../../products/domain/Product"; 4 | import { Either } from "../../../common/domain/Either"; 5 | import { DataError } from "../../../common/domain/DataError"; 6 | import { EitherAsync } from "../../../common/domain/EitherAsync"; 7 | 8 | export class AddProductToCartUseCase { 9 | private cartRepository: CartRepository; 10 | 11 | constructor(cartRepository: CartRepository) { 12 | this.cartRepository = cartRepository; 13 | } 14 | 15 | async execute(product: Product): Promise> { 16 | const cartResult = EitherAsync.fromPromise(this.cartRepository.get()); 17 | 18 | return cartResult 19 | .flatMap(async cart => { 20 | const cartItem = { 21 | id: product.id, 22 | image: product.image, 23 | title: product.title, 24 | price: product.price, 25 | quantity: 1, 26 | }; 27 | 28 | const editedCart = cart.addItem(cartItem); 29 | 30 | const saveResult = await this.cartRepository.save(editedCart); 31 | 32 | return saveResult.map(() => editedCart); 33 | }) 34 | .run(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/usecases/EditQuantityOfCartItemUseCase.ts: -------------------------------------------------------------------------------- 1 | import { CartRepository } from "../CartRepository"; 2 | import { Cart } from "../Cart"; 3 | import { EitherAsync } from "../../../common/domain/EitherAsync"; 4 | import { DataError } from "../../../common/domain/DataError"; 5 | import { Either } from "../../../common/domain/Either"; 6 | 7 | export class EditQuantityOfCartItemUseCase { 8 | private cartRepository: CartRepository; 9 | 10 | constructor(cartRepository: CartRepository) { 11 | this.cartRepository = cartRepository; 12 | } 13 | 14 | async execute(itemId: string, quantity: number): Promise> { 15 | const cartResult = EitherAsync.fromPromise(this.cartRepository.get()); 16 | 17 | return cartResult 18 | .flatMap(async cart => { 19 | const editedCart = cart.editItem(itemId, quantity); 20 | 21 | const saveResult = await this.cartRepository.save(editedCart); 22 | 23 | return saveResult.map(() => editedCart); 24 | }) 25 | .run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/usecases/GetCartUseCase.ts: -------------------------------------------------------------------------------- 1 | import { CartRepository } from "../CartRepository"; 2 | import { Cart } from "../Cart"; 3 | import { DataError } from "../../../common/domain/DataError"; 4 | import { Either } from "../../../common/domain/Either"; 5 | 6 | export class GetCartUseCase { 7 | private cartRepository: CartRepository; 8 | 9 | constructor(cartRepository: CartRepository) { 10 | this.cartRepository = cartRepository; 11 | } 12 | 13 | execute(): Promise> { 14 | return this.cartRepository.get(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/usecases/RemoveItemFromCartUseCase.ts: -------------------------------------------------------------------------------- 1 | import { CartRepository } from "../CartRepository"; 2 | import { Cart } from "../Cart"; 3 | import { Either } from "../../../common/domain/Either"; 4 | import { DataError } from "../../../common/domain/DataError"; 5 | import { EitherAsync } from "../../../common/domain/EitherAsync"; 6 | 7 | export class RemoveItemFromCartUseCase { 8 | private cartRepository: CartRepository; 9 | 10 | constructor(cartRepository: CartRepository) { 11 | this.cartRepository = cartRepository; 12 | } 13 | 14 | async execute(itemId: string): Promise> { 15 | const cartResult = EitherAsync.fromPromise(this.cartRepository.get()); 16 | 17 | return cartResult 18 | .flatMap(async cart => { 19 | const editedCart = cart.removeItem(itemId); 20 | 21 | const saveResult = await this.cartRepository.save(editedCart); 22 | 23 | return saveResult.map(() => editedCart); 24 | }) 25 | .run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/cart/domain/usecases/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AddProductToCartUseCase"; 2 | export * from "./EditQuantityOfCartItemUseCase"; 3 | export * from "./GetCartUseCase"; 4 | export * from "./RemoveItemFromCartUseCase"; 5 | -------------------------------------------------------------------------------- /packages/core/src/cart/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./data"; 2 | export * from "./domain"; 3 | export * from "./presentation"; 4 | -------------------------------------------------------------------------------- /packages/core/src/cart/presentation/CartPloc.ts: -------------------------------------------------------------------------------- 1 | import { CartState, cartInitialState, CartItemState } from "./CartState"; 2 | import { Ploc } from "../../common/presentation/Ploc"; 3 | import { GetCartUseCase } from "../domain/usecases/GetCartUseCase"; 4 | import { AddProductToCartUseCase } from "../domain/usecases/AddProductToCartUseCase"; 5 | import { RemoveItemFromCartUseCase } from "../domain/usecases/RemoveItemFromCartUseCase"; 6 | import { EditQuantityOfCartItemUseCase } from "../domain/usecases/EditQuantityOfCartItemUseCase"; 7 | import { Product } from "../../products/domain/Product"; 8 | import { Cart } from "../domain/Cart"; 9 | import { DataError } from "../../common/domain/DataError"; 10 | 11 | export class CartPloc extends Ploc { 12 | constructor( 13 | private getCartUseCase: GetCartUseCase, 14 | private addProductToCartUseCase: AddProductToCartUseCase, 15 | private removeItemFromCartUseCase: RemoveItemFromCartUseCase, 16 | private editQuantityOfCartItemUseCase: EditQuantityOfCartItemUseCase 17 | ) { 18 | super(cartInitialState); 19 | 20 | this.loadCart(); 21 | } 22 | 23 | closeCart() { 24 | this.changeState({ ...this.state, open: false }); 25 | } 26 | 27 | openCart() { 28 | this.changeState({ ...this.state, open: true }); 29 | } 30 | 31 | async removeCartItem(item: CartItemState) { 32 | const result = await this.removeItemFromCartUseCase.execute(item.id); 33 | 34 | result.fold( 35 | error => this.changeState(this.handleError(error)), 36 | cart => this.changeState(this.mapToUpdatedState(cart)) 37 | ); 38 | } 39 | 40 | async editQuantityCartItem(item: CartItemState, quantity: number) { 41 | const result = await this.editQuantityOfCartItemUseCase.execute(item.id, quantity); 42 | 43 | result.fold( 44 | error => this.changeState(this.handleError(error)), 45 | cart => this.changeState(this.mapToUpdatedState(cart)) 46 | ); 47 | } 48 | 49 | async addProductToCart(product: Product) { 50 | const result = await this.addProductToCartUseCase.execute(product); 51 | 52 | result.fold( 53 | error => this.changeState(this.handleError(error)), 54 | cart => this.changeState(this.mapToUpdatedState(cart)) 55 | ); 56 | } 57 | 58 | private async loadCart() { 59 | const result = await this.getCartUseCase.execute(); 60 | 61 | result.fold( 62 | error => this.changeState(this.handleError(error)), 63 | cart => this.changeState(this.mapToUpdatedState(cart)) 64 | ); 65 | } 66 | 67 | mapToUpdatedState(cart: Cart): CartState { 68 | const formatOptions = { style: "currency", currency: "EUR" }; 69 | 70 | return { 71 | kind: "UpdatedCartState", 72 | open: this.state.open, 73 | totalItems: cart.totalItems, 74 | totalPrice: cart.totalPrice.toLocaleString("es-ES", formatOptions), 75 | items: cart.items.map(cartItem => { 76 | return { 77 | id: cartItem.id, 78 | image: cartItem.image, 79 | title: cartItem.title, 80 | price: cartItem.price.toLocaleString("es-ES", formatOptions), 81 | quantity: cartItem.quantity, 82 | }; 83 | }), 84 | }; 85 | } 86 | 87 | private handleError(error: DataError): CartState { 88 | switch (error.kind) { 89 | case "UnexpectedError": { 90 | return { 91 | open: this.state.open, 92 | kind: "ErrorCartState", 93 | error: "Sorry, an error has ocurred. Please try later again", 94 | }; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/core/src/cart/presentation/CartState.ts: -------------------------------------------------------------------------------- 1 | export interface CommonCartState { 2 | open: boolean; 3 | } 4 | 5 | export interface LoadingCartState { 6 | kind: "LoadingCartState"; 7 | } 8 | 9 | export interface UpdatedCartState { 10 | kind: "UpdatedCartState"; 11 | items: Array; 12 | totalPrice: string; 13 | totalItems: number; 14 | } 15 | 16 | export interface ErrorCartState { 17 | kind: "ErrorCartState"; 18 | error: string; 19 | } 20 | 21 | export type CartState = (LoadingCartState | UpdatedCartState | ErrorCartState) & CommonCartState; 22 | 23 | export interface CartItemState { 24 | id: string; 25 | image: string; 26 | title: string; 27 | price: string; 28 | quantity: number; 29 | } 30 | 31 | export const cartInitialState: CartState = { 32 | kind: "LoadingCartState", 33 | open: false, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/core/src/cart/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./CartPloc"; 2 | export * from "./CartState"; 3 | -------------------------------------------------------------------------------- /packages/core/src/common/dependencies/DependenciesLocator.ts: -------------------------------------------------------------------------------- 1 | import { CartInMemoryRepository } from "../../cart/data/CartInMemoryRepository"; 2 | import { AddProductToCartUseCase } from "../../cart/domain/usecases/AddProductToCartUseCase"; 3 | import { EditQuantityOfCartItemUseCase } from "../../cart/domain/usecases/EditQuantityOfCartItemUseCase"; 4 | import { GetCartUseCase } from "../../cart/domain/usecases/GetCartUseCase"; 5 | import { RemoveItemFromCartUseCase } from "../../cart/domain/usecases/RemoveItemFromCartUseCase"; 6 | import { CartPloc } from "../../cart/presentation/CartPloc"; 7 | import { ProductInMemoryRepository } from "../../products/data/ProductInMemoryRepository"; 8 | import { GetProductsUseCase } from "../../products/domain/GetProductsUseCase"; 9 | import { ProductsPloc } from "../../products/presentation/ProductsPloc"; 10 | 11 | function provideProductsPloc(): ProductsPloc { 12 | const productRepository = new ProductInMemoryRepository(); 13 | const getProductsUseCase = new GetProductsUseCase(productRepository); 14 | const productsPloc = new ProductsPloc(getProductsUseCase); 15 | 16 | return productsPloc; 17 | } 18 | 19 | function provideCartPloc(): CartPloc { 20 | const cartRepository = new CartInMemoryRepository(); 21 | const getCartUseCase = new GetCartUseCase(cartRepository); 22 | const addProductToCartUseCase = new AddProductToCartUseCase(cartRepository); 23 | const removeItemFromCartUseCase = new RemoveItemFromCartUseCase(cartRepository); 24 | const editQuantityOfCartItemUseCase = new EditQuantityOfCartItemUseCase(cartRepository); 25 | const cartPloc = new CartPloc( 26 | getCartUseCase, 27 | addProductToCartUseCase, 28 | removeItemFromCartUseCase, 29 | editQuantityOfCartItemUseCase 30 | ); 31 | 32 | return cartPloc; 33 | } 34 | 35 | export const dependenciesLocator = { 36 | provideProductsPloc, 37 | provideCartPloc, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core/src/common/dependencies/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DependenciesLocator"; 2 | -------------------------------------------------------------------------------- /packages/core/src/common/domain/DataError.ts: -------------------------------------------------------------------------------- 1 | export interface UnexpectedError { 2 | kind: "UnexpectedError"; 3 | error: Error; 4 | } 5 | 6 | export type DataError = UnexpectedError; 7 | -------------------------------------------------------------------------------- /packages/core/src/common/domain/Either.ts: -------------------------------------------------------------------------------- 1 | type Left = { kind: "left"; leftValue: L }; 2 | type Right = { kind: "right"; rightValue: R }; 3 | 4 | type EitherValue = Left | Right; 5 | 6 | export class Either { 7 | private constructor(private readonly value: EitherValue) {} 8 | 9 | isLeft(): boolean { 10 | return this.value.kind === "left"; 11 | } 12 | isRight(): boolean { 13 | return this.value.kind === "right"; 14 | } 15 | 16 | fold(leftFn: (left: L) => T, rightFn: (right: R) => T): T { 17 | switch (this.value.kind) { 18 | case "left": 19 | return leftFn(this.value.leftValue); 20 | case "right": 21 | return rightFn(this.value.rightValue); 22 | } 23 | } 24 | 25 | map(fn: (r: R) => T): Either { 26 | return this.flatMap(r => Either.right(fn(r))); 27 | } 28 | 29 | flatMap(fn: (right: R) => Either): Either { 30 | return this.fold( 31 | leftValue => Either.left(leftValue), 32 | rightValue => fn(rightValue) 33 | ); 34 | } 35 | 36 | mapLeft(fn: (l: L) => T): Either { 37 | return this.flatMapLeft(l => Either.left(fn(l))); 38 | } 39 | 40 | flatMapLeft(fn: (left: L) => Either): Either { 41 | return this.fold( 42 | leftValue => fn(leftValue), 43 | rightValue => Either.right(rightValue) 44 | ); 45 | } 46 | 47 | get(errorMessage?: string): R { 48 | return this.getOrThrow(errorMessage); 49 | } 50 | 51 | getOrThrow(errorMessage?: string): R { 52 | const throwFn = () => { 53 | throw Error( 54 | errorMessage 55 | ? errorMessage 56 | : "An error has ocurred retrieving value: " + JSON.stringify(this.value) 57 | ); 58 | }; 59 | 60 | return this.fold( 61 | () => throwFn(), 62 | rightValue => rightValue 63 | ); 64 | } 65 | 66 | getLeft(): L { 67 | const throwFn = () => { 68 | throw Error("The value is right: " + JSON.stringify(this.value)); 69 | }; 70 | 71 | return this.fold( 72 | leftValue => leftValue, 73 | () => throwFn() 74 | ); 75 | } 76 | 77 | getOrElse(defaultValue: R): R { 78 | return this.fold( 79 | () => defaultValue, 80 | someValue => someValue 81 | ); 82 | } 83 | 84 | static left(value: L) { 85 | return new Either({ kind: "left", leftValue: value }); 86 | } 87 | 88 | static right(value: R) { 89 | return new Either({ kind: "right", rightValue: value }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/core/src/common/domain/EitherAsync.ts: -------------------------------------------------------------------------------- 1 | import { Either } from "./Either"; 2 | 3 | export class EitherAsync { 4 | private constructor(private readonly promiseValue: () => Promise>) {} 5 | 6 | map(fn: (r: R) => T): EitherAsync { 7 | return this.flatMap(async r => Either.right(fn(r))); 8 | } 9 | 10 | flatMap(fn: (right: R) => Promise>): EitherAsync { 11 | return new EitherAsync(async () => { 12 | const value = await this.promiseValue(); 13 | 14 | return value.fold( 15 | async rightValue => Either.left(rightValue), 16 | rightValue => fn(rightValue) 17 | ); 18 | }); 19 | } 20 | 21 | mapLeft(fn: (l: L) => T): EitherAsync { 22 | return this.flatMapLeft(async l => Either.left(fn(l))); 23 | } 24 | 25 | flatMapLeft(fn: (left: L) => Promise>): EitherAsync { 26 | return new EitherAsync(async () => { 27 | const value = await this.promiseValue(); 28 | 29 | return value.fold( 30 | leftValue => fn(leftValue), 31 | async rightValue => Either.right(rightValue) 32 | ); 33 | }); 34 | } 35 | 36 | run(): Promise> { 37 | return this.promiseValue(); 38 | } 39 | 40 | static fromEither(value: Either): EitherAsync { 41 | return new EitherAsync(() => Promise.resolve(value)); 42 | } 43 | 44 | static fromPromise(value: Promise>): EitherAsync { 45 | return new EitherAsync(() => value); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./presentation"; 2 | export * from "./dependencies"; 3 | -------------------------------------------------------------------------------- /packages/core/src/common/presentation/Ploc.ts: -------------------------------------------------------------------------------- 1 | type Subscription = (state: S) => void; 2 | 3 | export abstract class Ploc { 4 | private internalState: S; 5 | private listeners: Subscription[] = []; 6 | 7 | constructor(initalState: S) { 8 | this.internalState = initalState; 9 | } 10 | 11 | public get state(): S { 12 | return this.internalState; 13 | } 14 | 15 | changeState(state: S) { 16 | this.internalState = state; 17 | 18 | if (this.listeners.length > 0) { 19 | this.listeners.forEach(listener => listener(this.state)); 20 | } 21 | } 22 | 23 | subscribe(listener: Subscription) { 24 | this.listeners.push(listener); 25 | } 26 | 27 | unsubscribe(listener: Subscription) { 28 | const index = this.listeners.indexOf(listener); 29 | if (index > -1) { 30 | this.listeners.splice(index, 1); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/common/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Ploc"; 2 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cart"; 2 | export * from "./common"; 3 | export * from "./products"; 4 | -------------------------------------------------------------------------------- /packages/core/src/products/data/ProductInMemoryRepository.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from "../domain/ProductRepository"; 2 | import { Product } from "../domain/Product"; 3 | import { Either } from "../../common/domain/Either"; 4 | import { DataError } from "../../common/domain/DataError"; 5 | 6 | const products = [ 7 | { 8 | id: "1", 9 | image: "https://images-na.ssl-images-amazon.com/images/I/71Y1S1m-QAL._AC_UY879_.jpg", 10 | title: "Element Blazin LS tee Shirt, Hombre", 11 | price: 19.95, 12 | }, 13 | { 14 | id: "2", 15 | image: "https://m.media-amazon.com/images/I/81HnHYik58L._AC_UL640_FMwebp_QL65_.jpg", 16 | title: "Element Vertical SS tee Shirt, Hombre", 17 | price: 21.95, 18 | }, 19 | { 20 | id: "3", 21 | image: "https://m.media-amazon.com/images/I/81ZYZ9yl1hL._AC_UL640_FMwebp_QL65_.jpg", 22 | title: 'Element Skater Backpack Mohave 15" Saison ', 23 | price: 52.45, 24 | }, 25 | { 26 | id: "4", 27 | image: "https://m.media-amazon.com/images/I/61-DwEh1zrL._AC_UL640_FMwebp_QL65_.jpg", 28 | title: "Element Indiana Logo N1SSA5ELP9", 29 | price: 18.9, 30 | }, 31 | { 32 | id: "5", 33 | image: "https://m.media-amazon.com/images/I/81SgdrnVNJL._AC_UL320_.jpg", 34 | title: "Basic Pocket Label Camisetas Element Hombre", 35 | price: 27.95, 36 | }, 37 | { 38 | id: "6", 39 | image: "https://m.media-amazon.com/images/I/81giLCXfxIL._AC_UL640_FMwebp_QL65_.jpg", 40 | title: "Element N2ssa2 Camiseta, Niños", 41 | price: 13.9, 42 | }, 43 | { 44 | id: "7", 45 | image: "https://m.media-amazon.com/images/I/61S2KdoGNnL._AC_UL320_.jpg", 46 | title: "Joint - Camiseta para Hombre Camiseta Element Hombre", 47 | price: 32.0, 48 | }, 49 | { 50 | id: "8", 51 | image: "https://m.media-amazon.com/images/I/7119OAEE+gL._AC_UL640_FMwebp_QL65_.jpg", 52 | title: "Element Alder Light 2 Tones", 53 | price: 68.35, 54 | }, 55 | { 56 | id: "9", 57 | image: "https://m.media-amazon.com/images/I/71dp5f24TbL._AC_UL640_FMwebp_QL65_.jpg", 58 | title: 'Element Skater Backpack Mohave 15" Season', 59 | price: 52.84, 60 | }, 61 | { 62 | id: "10", 63 | image: "https://m.media-amazon.com/images/I/71Kj-jV5v8L._AC_UL640_FMwebp_QL65_.jpg", 64 | title: "Element Vertical SS Camiseta, Niños", 65 | price: 13.9, 66 | }, 67 | { 68 | id: "11", 69 | image: "https://m.media-amazon.com/images/I/71jlppwpjmL._AC_UL640_FMwebp_QL65_.jpg", 70 | title: "Element Alder Heavy Puff TW Chaqueta, Hombre, Verde Oliva, M EU", 71 | price: 168.75, 72 | }, 73 | { 74 | id: "12", 75 | image: "https://m.media-amazon.com/images/I/71BSdq6OzDL._AC_UL640_FMwebp_QL65_.jpg", 76 | title: "Element Hombre Meridian Block Sudadera Mid Grey HTR", 77 | price: 47.5, 78 | }, 79 | { 80 | id: "13", 81 | image: "https://m.media-amazon.com/images/I/81RAeKF-8wL._AC_UL640_FMwebp_QL65_.jpg", 82 | title: "Element Sudadera - para Hombre", 83 | price: 64.94, 84 | }, 85 | { 86 | id: "14", 87 | image: "https://m.media-amazon.com/images/I/717tHbEHDnL._AC_UL640_FMwebp_QL65_.jpg", 88 | title: "Element Hombre Camiseta t-Shirt Signature", 89 | price: 29.84, 90 | }, 91 | { 92 | id: "15", 93 | image: "https://m.media-amazon.com/images/I/81rOs3LA0LL._AC_UL640_FMwebp_QL65_.jpg", 94 | title: "Element Section' Pre-Built Complete - 7.50\"", 95 | price: 99.0, 96 | }, 97 | { 98 | id: "16", 99 | image: "https://m.media-amazon.com/images/I/61-xQZORAKL._AC_UL640_FMwebp_QL65_.jpg", 100 | title: "Element Camiseta - para hombre", 101 | price: 27.06, 102 | }, 103 | { 104 | id: "17", 105 | image: "https://m.media-amazon.com/images/I/71RUdoglJML._AC_UL640_FMwebp_QL65_.jpg", 106 | title: "Element Alder Light", 107 | price: 86.52, 108 | }, 109 | { 110 | id: "18", 111 | image: "https://m.media-amazon.com/images/I/714tTmj4KvL._AC_UL640_FMwebp_QL65_.jpg", 112 | title: "Element Chaqueta Alder Puff TW Negro", 113 | price: 73.5, 114 | }, 115 | ]; 116 | 117 | export class ProductInMemoryRepository implements ProductRepository { 118 | get(filter: string): Promise> { 119 | return new Promise((resolve, _reject) => { 120 | setTimeout(() => { 121 | try { 122 | if (filter) { 123 | const filteredProducts = products.filter((p: Product) => { 124 | return p.title.toLowerCase().includes(filter.toLowerCase()); 125 | }); 126 | 127 | resolve(Either.right(filteredProducts)); 128 | } else { 129 | resolve(Either.right(products)); 130 | } 131 | } catch (error) { 132 | resolve(Either.left({ kind: "UnexpectedError", error })); 133 | } 134 | }, 100); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/core/src/products/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ProductInMemoryRepository"; 2 | -------------------------------------------------------------------------------- /packages/core/src/products/domain/GetProductsUseCase.ts: -------------------------------------------------------------------------------- 1 | import { ProductRepository } from "./ProductRepository"; 2 | import { Product } from "./Product"; 3 | import { Either } from "../../common/domain/Either"; 4 | import { DataError } from "../../common/domain/DataError"; 5 | 6 | export class GetProductsUseCase { 7 | private productRepository: ProductRepository; 8 | 9 | constructor(productRepository: ProductRepository) { 10 | this.productRepository = productRepository; 11 | } 12 | 13 | execute(filter: string): Promise> { 14 | return this.productRepository.get(filter); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/products/domain/Product.ts: -------------------------------------------------------------------------------- 1 | export type ProductId = string; 2 | export type Image = string; 3 | export type Title = string; 4 | export type Price = number; 5 | 6 | export interface Product { 7 | id: ProductId; 8 | image: Image; 9 | title: Title; 10 | price: Price; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/products/domain/ProductRepository.ts: -------------------------------------------------------------------------------- 1 | import { DataError } from "../../common/domain/DataError"; 2 | import { Either } from "../../common/domain/Either"; 3 | import { Product } from "./Product"; 4 | 5 | export interface ProductRepository { 6 | get(filter: string): Promise>; 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/products/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./GetProductsUseCase"; 2 | export * from "./ProductRepository"; 3 | export * from "./Product"; 4 | -------------------------------------------------------------------------------- /packages/core/src/products/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./data"; 2 | export * from "./domain"; 3 | export * from "./presentation"; 4 | -------------------------------------------------------------------------------- /packages/core/src/products/presentation/ProductsPloc.ts: -------------------------------------------------------------------------------- 1 | import { DataError } from "../../common/domain/DataError"; 2 | import { Ploc } from "../../common/presentation/Ploc"; 3 | import { GetProductsUseCase } from "../domain/GetProductsUseCase"; 4 | import { productsInitialState, ProductsState } from "./ProductsState"; 5 | 6 | export class ProductsPloc extends Ploc { 7 | constructor(private getProductsUseCase: GetProductsUseCase) { 8 | super(productsInitialState); 9 | } 10 | 11 | async search(searchTerm: string) { 12 | const productResult = await this.getProductsUseCase.execute(searchTerm); 13 | 14 | productResult.fold( 15 | error => this.changeState(this.handleError(searchTerm, error)), 16 | products => 17 | this.changeState({ 18 | kind: "LoadedProductsState", 19 | products, 20 | searchTerm, 21 | }) 22 | ); 23 | } 24 | 25 | private handleError(searchTerm: string, error: DataError): ProductsState { 26 | switch (error.kind) { 27 | case "UnexpectedError": { 28 | return { 29 | searchTerm, 30 | kind: "ErrorProductsState", 31 | error: "Sorry, an error has ocurred. Please try later again", 32 | }; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/products/presentation/ProductsState.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "../domain/Product"; 2 | 3 | export interface CommonProductsState { 4 | searchTerm: string; 5 | } 6 | 7 | export interface LoadingProductsState { 8 | kind: "LoadingProductsState"; 9 | } 10 | 11 | export interface LoadedProductsState { 12 | kind: "LoadedProductsState"; 13 | products: Array; 14 | } 15 | 16 | export interface ErrorProductsState { 17 | kind: "ErrorProductsState"; 18 | error: string; 19 | } 20 | 21 | export type ProductsState = (LoadingProductsState | LoadedProductsState | ErrorProductsState) & 22 | CommonProductsState; 23 | 24 | export const productsInitialState: ProductsState = { 25 | kind: "LoadingProductsState", 26 | searchTerm: "", 27 | }; 28 | -------------------------------------------------------------------------------- /packages/core/src/products/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ProductsPloc"; 2 | export * from "./ProductsState"; 3 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./build", 5 | "rootDir": "./src", 6 | "composite": true, 7 | "declaration": true, 8 | "allowJs": false, 9 | "target": "es5", 10 | "sourceMap": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "esModuleInterop": true 14 | }, 15 | "include": [ 16 | "./src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "**/*.spec.ts" 21 | ], 22 | } -------------------------------------------------------------------------------- /packages/react-app/.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/index.tsx":"1","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/reportWebVitals.ts":"2","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/theme.tsx":"3","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/app/App.tsx":"4","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/appbar/MyAppBar.tsx":"5","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/common/Context.tsx":"6","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/products/ProductList.tsx":"7","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/cart/CartDrawer.tsx":"8","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/cart/CartContent.tsx":"9","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/cart/CartContentItem.tsx":"10","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/products/ProductItem.tsx":"11","/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/common/usePlocState.tsx":"12"},{"size":791,"mtime":1610380525536,"results":"13","hashOfConfig":"14"},{"size":467,"mtime":1607328457902,"results":"15","hashOfConfig":"14"},{"size":470,"mtime":1610380525537,"results":"16","hashOfConfig":"14"},{"size":660,"mtime":1610477046813,"results":"17","hashOfConfig":"14"},{"size":1383,"mtime":1619706821980,"results":"18","hashOfConfig":"14"},{"size":364,"mtime":1610380525535,"results":"19","hashOfConfig":"14"},{"size":2492,"mtime":1619706821980,"results":"20","hashOfConfig":"14"},{"size":1941,"mtime":1619706821980,"results":"21","hashOfConfig":"14"},{"size":2958,"mtime":1619706821981,"results":"22","hashOfConfig":"14"},{"size":2995,"mtime":1610380525535,"results":"23","hashOfConfig":"14"},{"size":2317,"mtime":1610380525536,"results":"24","hashOfConfig":"14"},{"size":458,"mtime":1619706819107,"results":"25","hashOfConfig":"14"},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},"187ro8z",{"filePath":"29","messages":"30","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"31","messages":"32","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"33","messages":"34","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"35","messages":"36","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"37","messages":"38","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"39","messages":"40","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"41","messages":"42","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"43","messages":"44","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"45","messages":"46","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},{"filePath":"47","messages":"48","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"28"},"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/index.tsx",[],["51","52"],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/reportWebVitals.ts",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/theme.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/app/App.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/appbar/MyAppBar.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/common/Context.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/products/ProductList.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/cart/CartDrawer.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/cart/CartContent.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/cart/CartContentItem.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/products/ProductItem.tsx",[],"/Users/xurxodev/Workspace/xurxodev/blog/frontend-clean-architecture/packages/react-app/src/common/usePlocState.tsx",[],{"ruleId":"53","replacedBy":"54"},{"ruleId":"55","replacedBy":"56"},"no-native-reassign",["57"],"no-negated-in-lhs",["58"],"no-global-assign","no-unsafe-negation"] -------------------------------------------------------------------------------- /packages/react-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | env: { 4 | node: true, 5 | }, 6 | extends: ["../../.eslintrc.js", "react-app", "plugin:react/recommended"], 7 | parserOptions: { 8 | ecmaVersion: 2018, 9 | sourceType: "module", 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | rules: { 15 | "react/prop-types": "off", 16 | "react-hooks/exhaustive-deps": "off", 17 | "react/jsx-uses-react": "off", 18 | "react/react-in-jsx-scope": "off", 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/react-app/README.md: -------------------------------------------------------------------------------- 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | -------------------------------------------------------------------------------- /packages/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-clean-architecture/react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@frontend-clean-architecture/core": "0.1.0", 7 | "@material-ui/core": "^4.11.2", 8 | "@material-ui/icons": "^4.11.2", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "4.0.1", 12 | "web-vitals": "^1.0.1" 13 | }, 14 | "devDependencies": { 15 | "@testing-library/jest-dom": "^5.11.4", 16 | "@testing-library/react": "^11.1.0", 17 | "@testing-library/user-event": "^12.1.10", 18 | "@types/jest": "^26.0.15", 19 | "@types/node": "^14.14.10", 20 | "@types/react": "^16.9.53", 21 | "@types/react-dom": "^16.9.8", 22 | "eslint-config-react-app": "^6.0.0", 23 | "eslint-plugin-react": "^7.21.5", 24 | "eslint-plugin-react-hooks": "^4.2.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test --watchAll=false", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } -------------------------------------------------------------------------------- /packages/react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/react-app/public/favicon.ico -------------------------------------------------------------------------------- /packages/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 | -------------------------------------------------------------------------------- /packages/react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/react-app/public/logo192.png -------------------------------------------------------------------------------- /packages/react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/react-app/public/logo512.png -------------------------------------------------------------------------------- /packages/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 | -------------------------------------------------------------------------------- /packages/react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/react-app/src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MyAppBar from "../appbar/MyAppBar"; 3 | import { CartPloc, dependenciesLocator } from "@frontend-clean-architecture/core"; 4 | import { createContext } from "../common/Context"; 5 | import ProductList from "../products/ProductList"; 6 | import CartDrawer from "../cart/CartDrawer"; 7 | 8 | const [blocContext, usePloc] = createContext(); 9 | 10 | export const useCartPloc = usePloc; 11 | 12 | const App: React.FC = () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /packages/react-app/src/app/__test__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "../App"; 3 | 4 | test("renders learn react link", async () => { 5 | render(); 6 | await screen.findByText("Results for"); 7 | await screen.findByText("Element"); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/react-app/src/appbar/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/react-app/src/appbar/Logo.png -------------------------------------------------------------------------------- /packages/react-app/src/appbar/MyAppBar.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from "@material-ui/core/styles"; 2 | import AppBar from "@material-ui/core/AppBar"; 3 | import Toolbar from "@material-ui/core/Toolbar"; 4 | import IconButton from "@material-ui/core/IconButton"; 5 | import Badge from "@material-ui/core/Badge"; 6 | import ShoppingCartIcon from "@material-ui/icons/ShoppingCart"; 7 | import logo from "./logo.png"; 8 | import reactLogo from "./react-logo.png"; 9 | import { useCartPloc } from "../app/App"; 10 | import { usePlocState } from "../common/usePlocState"; 11 | 12 | const useStyles = makeStyles(() => ({ 13 | toolbar: { 14 | justifyContent: "space-between", 15 | maxWidth: "800", 16 | }, 17 | })); 18 | 19 | const MyAppBar: React.FC = () => { 20 | const classes = useStyles(); 21 | const ploc = useCartPloc(); 22 | const state = usePlocState(ploc); 23 | 24 | const totalItems = state.kind === "UpdatedCartState" ? state.totalItems : 0; 25 | 26 | return ( 27 | 28 | 29 |
30 | logo 31 | react logo 32 |
33 | 34 | 35 | 36 | ploc.openCart()} /> 37 | 38 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default MyAppBar; 45 | -------------------------------------------------------------------------------- /packages/react-app/src/appbar/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/react-app/src/appbar/react-logo.png -------------------------------------------------------------------------------- /packages/react-app/src/cart/CartContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles, Theme } from "@material-ui/core/styles"; 3 | import { List, Divider, Box, Typography, CircularProgress } from "@material-ui/core"; 4 | import CartContentItem from "./CartContentItem"; 5 | import { CartItemState } from "@frontend-clean-architecture/core"; 6 | import { useCartPloc } from "../app/App"; 7 | import { usePlocState } from "../common/usePlocState"; 8 | 9 | const useStyles = makeStyles((theme: Theme) => ({ 10 | totalPriceContainer: { 11 | display: "flex", 12 | alignItems: "center", 13 | padding: theme.spacing(1, 0), 14 | justifyContent: "space-around", 15 | }, 16 | itemsContainer: { 17 | display: "flex", 18 | alignItems: "center", 19 | padding: theme.spacing(1, 0), 20 | justifyContent: "space-around", 21 | minHeight: 150, 22 | }, 23 | itemsList: { 24 | overflow: "scroll", 25 | }, 26 | infoContainer: { 27 | display: "flex", 28 | alignItems: "center", 29 | justifyContent: "center", 30 | height: "100vh", 31 | }, 32 | })); 33 | 34 | const CartContent: React.FC = () => { 35 | const classes = useStyles(); 36 | const ploc = useCartPloc(); 37 | const state = usePlocState(ploc); 38 | 39 | const cartItems = (items: CartItemState[]) => ( 40 | 41 | {items.map((item, index) => ( 42 | 43 | ))} 44 | 45 | ); 46 | 47 | const emptyCartItems = () => ( 48 | 49 | 50 | Empty Cart :( 51 | 52 | 53 | ); 54 | 55 | switch (state.kind) { 56 | case "LoadingCartState": { 57 | return ( 58 |
59 | 60 |
61 | ); 62 | } 63 | case "ErrorCartState": { 64 | return ( 65 |
66 | 67 | {state.error} 68 | 69 |
70 | ); 71 | } 72 | case "UpdatedCartState": { 73 | return ( 74 | 75 | 76 | {state.items.length > 0 ? cartItems(state.items) : emptyCartItems()} 77 | 78 | 79 | 80 | 81 | Total Price 82 | 83 | 84 | {state.totalPrice} 85 | 86 | 87 | 88 | ); 89 | } 90 | } 91 | }; 92 | 93 | export default CartContent; 94 | -------------------------------------------------------------------------------- /packages/react-app/src/cart/CartContentItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { Key } from "react"; 2 | import { makeStyles, Theme } from "@material-ui/core/styles"; 3 | import { 4 | ListItem, 5 | ListItemText, 6 | ListItemSecondaryAction, 7 | IconButton, 8 | TextField, 9 | Paper, 10 | Box, 11 | Typography, 12 | } from "@material-ui/core"; 13 | import RemoveIcon from "@material-ui/icons/Clear"; 14 | import { useCartPloc } from "../app/App"; 15 | import { CartItemState } from "@frontend-clean-architecture/core"; 16 | 17 | const useStyles = makeStyles((theme: Theme) => ({ 18 | itemContainer: { 19 | margin: theme.spacing(1), 20 | }, 21 | itemImage: { 22 | padding: theme.spacing(0, 1), 23 | backgroundSize: "auto 100%", 24 | }, 25 | secondContainer: { 26 | display: "flex", 27 | alignItems: "center", 28 | padding: theme.spacing(1, 0), 29 | justifyContent: "space-around", 30 | }, 31 | quantityField: { 32 | marginTop: theme.spacing(1), 33 | width: 60, 34 | }, 35 | })); 36 | 37 | interface CartProps { 38 | key: Key; 39 | cartItem: CartItemState; 40 | } 41 | 42 | const CartContentItem: React.FC = ({ key, cartItem }) => { 43 | const classes = useStyles(); 44 | const bloc = useCartPloc(); 45 | 46 | return ( 47 | 48 | 49 | 50 | {cartItem.title} 56 | 60 | 71 | bloc.editQuantityCartItem(cartItem, +event.target.value) 72 | } 73 | /> 74 | {cartItem.price} 75 | 76 | } 77 | /> 78 | 79 | 80 | bloc.removeCartItem(cartItem)} /> 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default CartContentItem; 90 | -------------------------------------------------------------------------------- /packages/react-app/src/cart/CartDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles, Theme } from "@material-ui/core/styles"; 3 | import { Drawer, IconButton, Divider, Typography, Box } from "@material-ui/core"; 4 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; 5 | import ShoppingCartIcon from "@material-ui/icons/ShoppingCart"; 6 | import CartContent from "./CartContent"; 7 | import { useCartPloc } from "../app/App"; 8 | import { usePlocState } from "../common/usePlocState"; 9 | 10 | const drawerWidth = 350; 11 | 12 | const useStyles = makeStyles((theme: Theme) => ({ 13 | drawer: { 14 | width: drawerWidth, 15 | }, 16 | drawerPaper: { 17 | width: drawerWidth, 18 | }, 19 | drawerHeader: { 20 | display: "flex", 21 | alignItems: "center", 22 | padding: theme.spacing(1, 0), 23 | justifyContent: "flex-start", 24 | }, 25 | drawerTitleContainer: { 26 | width: "100%", 27 | display: "flex", 28 | alignItems: "center", 29 | justifyContent: "center", 30 | }, 31 | drawerTitleIcon: { 32 | marginRight: theme.spacing(1), 33 | }, 34 | })); 35 | 36 | const CartDrawer: React.FC = () => { 37 | const classes = useStyles(); 38 | const ploc = useCartPloc(); 39 | const state = usePlocState(ploc); 40 | 41 | return ( 42 | 49 | 50 | ploc.closeCart()}> 51 | 52 | 53 | 54 | 55 | 56 | Cart 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default CartDrawer; 67 | -------------------------------------------------------------------------------- /packages/react-app/src/common/Context.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function createContext() { 4 | const context = React.createContext(undefined); 5 | 6 | function useContext() { 7 | const ctx = React.useContext(context); 8 | if (!ctx) throw new Error("context must be inside a Provider with a value"); 9 | return ctx; 10 | } 11 | return [context, useContext] as const; 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-app/src/common/usePlocState.tsx: -------------------------------------------------------------------------------- 1 | import { Ploc } from "@frontend-clean-architecture/core"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export function usePlocState(ploc: Ploc) { 5 | const [state, setState] = useState(ploc.state); 6 | 7 | useEffect(() => { 8 | const stateSubscription = (state: S) => { 9 | setState(state); 10 | }; 11 | 12 | ploc.subscribe(stateSubscription); 13 | 14 | return () => ploc.unsubscribe(stateSubscription); 15 | }, [ploc]); 16 | 17 | return state; 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app/App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import theme from "./theme"; 6 | import { CssBaseline, ThemeProvider } from "@material-ui/core"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 12 | 13 | 14 | 15 | , 16 | , 17 | document.getElementById("root") 18 | ); 19 | 20 | // If you want to start measuring performance in your app, pass a function 21 | // to log results (for example: reportWebVitals(console.log)) 22 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 23 | reportWebVitals(); 24 | -------------------------------------------------------------------------------- /packages/react-app/src/products/ProductItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { 4 | Grid, 5 | Card, 6 | CardMedia, 7 | CardContent, 8 | Typography, 9 | CardActions, 10 | Button, 11 | } from "@material-ui/core"; 12 | import { Product } from "@frontend-clean-architecture/core"; 13 | import { useCartPloc } from "../app/App"; 14 | 15 | const useStyles = makeStyles(theme => ({ 16 | card: { 17 | height: "100%", 18 | display: "flex", 19 | flexDirection: "column", 20 | }, 21 | cardMedia: { 22 | backgroundSize: "auto 100%", 23 | paddingTop: "100%", // 16:9, 24 | margin: theme.spacing(1), 25 | }, 26 | cardContent: { 27 | flexGrow: 1, 28 | }, 29 | cardActions: { 30 | justifyContent: "center", 31 | }, 32 | productTitle: { 33 | overflow: "hidden", 34 | textOverflow: "ellipsis", 35 | height: 50, 36 | }, 37 | productPrice: { 38 | textAlign: "center", 39 | }, 40 | })); 41 | 42 | interface ProductListProps { 43 | product: Product; 44 | } 45 | 46 | const ProductItem: React.FC = ({ product }) => { 47 | const classes = useStyles(); 48 | const bloc = useCartPloc(); 49 | 50 | return ( 51 | 52 | 53 | 58 | 59 | 60 | {product.title} 61 | 62 | 63 | {product.price.toLocaleString("es-ES", { 64 | style: "currency", 65 | currency: "EUR", 66 | })} 67 | 68 | 69 | 70 | 76 | 77 | 78 | 79 | ); 80 | }; 81 | 82 | export default ProductItem; 83 | -------------------------------------------------------------------------------- /packages/react-app/src/products/ProductList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { CircularProgress, Grid, Container, Box, Typography } from "@material-ui/core"; 4 | import ProductItem from "./ProductItem"; 5 | import { dependenciesLocator } from "@frontend-clean-architecture/core"; 6 | import { usePlocState } from "../common/usePlocState"; 7 | const useStyles = makeStyles(theme => ({ 8 | titleContainer: { 9 | marginBottom: theme.spacing(4), 10 | }, 11 | cardGrid: { 12 | paddingTop: theme.spacing(4), 13 | paddingBottom: theme.spacing(4), 14 | }, 15 | infoContainer: { 16 | display: "flex", 17 | alignItems: "center", 18 | justifyContent: "center", 19 | height: "100vh", 20 | }, 21 | })); 22 | 23 | const ProductList: React.FC = () => { 24 | const ploc = dependenciesLocator.provideProductsPloc(); 25 | const classes = useStyles(); 26 | const state = usePlocState(ploc); 27 | 28 | React.useEffect(() => { 29 | const searchProducts = async (filter: string) => { 30 | ploc.search(filter); 31 | }; 32 | 33 | searchProducts("Element"); 34 | }, [ploc]); 35 | 36 | switch (state.kind) { 37 | case "LoadingProductsState": { 38 | return ( 39 |
40 | 41 |
42 | ); 43 | } 44 | case "ErrorProductsState": { 45 | return ( 46 |
47 | 48 | {state.error} 49 | 50 |
51 | ); 52 | } 53 | case "LoadedProductsState": { 54 | return ( 55 | 56 | 57 | 58 | {"Results for "} 59 | 60 | 61 | {"Element"} 62 | 63 | 64 | 65 | {state.products.map((product, index) => ( 66 | 67 | ))} 68 | 69 | 70 | ); 71 | } 72 | } 73 | }; 74 | 75 | export default ProductList; 76 | -------------------------------------------------------------------------------- /packages/react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/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 | -------------------------------------------------------------------------------- /packages/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 | -------------------------------------------------------------------------------- /packages/react-app/src/theme.tsx: -------------------------------------------------------------------------------- 1 | import { red, blue, grey } from "@material-ui/core/colors"; 2 | import { createMuiTheme } from "@material-ui/core/styles"; 3 | 4 | // A custom theme for this app 5 | const theme = createMuiTheme({ 6 | palette: { 7 | primary: { 8 | main: blue.A400, 9 | }, 10 | secondary: { 11 | main: red.A700, 12 | }, 13 | error: { 14 | main: red.A400, 15 | }, 16 | background: { 17 | default: grey[50], 18 | }, 19 | }, 20 | }); 21 | 22 | export default theme; 23 | -------------------------------------------------------------------------------- /packages/react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "noFallthroughCasesInSwitch": true, 10 | "module": "esnext", 11 | "noEmit": true, 12 | "jsx": "react-jsx" 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } -------------------------------------------------------------------------------- /packages/vue-app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /packages/vue-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | extends: [ 6 | "../../.eslintrc.js", 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint", 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 19 | "no-redeclare": "off", 20 | "@typescript-eslint/no-redeclare": "off", 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/vue-app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /packages/vue-app/README.md: -------------------------------------------------------------------------------- 1 | # vue-app 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /packages/vue-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/vue-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@frontend-clean-architecture/vue-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "primeflex": "^2.0.0", 12 | "primeicons": "^4.1.0", 13 | "primevue": "^3.3.5", 14 | "vue": "^3.0.0" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.5.0", 18 | "@vue/cli-plugin-eslint": "~4.5.0", 19 | "@vue/cli-plugin-typescript": "~4.5.0", 20 | "@vue/cli-service": "~4.5.0", 21 | "@vue/compiler-sfc": "^3.0.0", 22 | "@vue/eslint-config-prettier": "^6.0.0", 23 | "@vue/eslint-config-typescript": "^5.0.2", 24 | "eslint-plugin-prettier": "^3.1.3", 25 | "eslint-plugin-vue": "^7.0.0-0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/vue-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/vue-app/public/favicon.ico -------------------------------------------------------------------------------- /packages/vue-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= htmlWebpackPlugin.options.title %> 11 | 12 | 17 | 18 | 19 | 20 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/vue-app/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /packages/vue-app/src/appbar/MyAppBar.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /packages/vue-app/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/vue-app/src/assets/logo.png -------------------------------------------------------------------------------- /packages/vue-app/src/assets/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/frontend-clean-architecture/3f3d150e9af169f3da53a3e6c0ff008771ff283b/packages/vue-app/src/assets/vue-logo.png -------------------------------------------------------------------------------- /packages/vue-app/src/cart/CartContent.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 39 | 40 | 61 | -------------------------------------------------------------------------------- /packages/vue-app/src/cart/CartContenttItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 80 | 81 | 121 | -------------------------------------------------------------------------------- /packages/vue-app/src/cart/CartSidebar.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 47 | 48 | 71 | -------------------------------------------------------------------------------- /packages/vue-app/src/common/usePlocState.ts: -------------------------------------------------------------------------------- 1 | import { Ploc } from "@frontend-clean-architecture/core"; 2 | 3 | import { DeepReadonly, onMounted, onUnmounted, readonly, Ref, ref } from "vue"; 4 | 5 | export function usePlocState(ploc: Ploc): DeepReadonly> { 6 | const state = ref(ploc.state) as Ref; 7 | 8 | const stateSubscription = (newState: S) => { 9 | state.value = newState; 10 | }; 11 | 12 | onMounted(() => { 13 | ploc.subscribe(stateSubscription); 14 | }); 15 | 16 | onUnmounted(() => { 17 | ploc.unsubscribe(stateSubscription); 18 | }); 19 | 20 | return readonly(state); 21 | } 22 | -------------------------------------------------------------------------------- /packages/vue-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import PrimeVue from "primevue/config"; 4 | 5 | import "primeflex/primeflex.css"; 6 | import "primevue/resources/themes/saga-blue/theme.css"; 7 | import "primevue/resources/primevue.min.css"; 8 | import "primeicons/primeicons.css"; 9 | 10 | import Toolbar from "primevue/toolbar"; 11 | import Button from "primevue/button"; 12 | import Splitbutton from "primevue/splitbutton"; 13 | import ProgressSpinner from "primevue/progressspinner"; 14 | import Card from "primevue/card"; 15 | import Sidebar from "primevue/sidebar"; 16 | import Divider from "primevue/divider"; 17 | import InputNumber from "primevue/inputnumber"; 18 | 19 | const app = createApp(App); 20 | 21 | app.use(PrimeVue); 22 | 23 | app.component("Toolbar", Toolbar); 24 | app.component("Button", Button); 25 | app.component("Splitbutton", Splitbutton); 26 | 27 | app.component("ProgressSpinner", ProgressSpinner); 28 | app.component("Card", Card); 29 | 30 | app.component("Sidebar", Sidebar); 31 | app.component("Divider", Divider); 32 | app.component("InputNumber", InputNumber); 33 | 34 | app.mount("#app"); 35 | -------------------------------------------------------------------------------- /packages/vue-app/src/products/ProductItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 64 | 65 | 99 | -------------------------------------------------------------------------------- /packages/vue-app/src/products/ProductList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 49 | 50 | 61 | -------------------------------------------------------------------------------- /packages/vue-app/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import { DefineComponent } from "vue"; 3 | const component: DefineComponent, Record, unknown>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /packages/vue-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "baseUrl": ".", 11 | "types": [ 12 | "webpack-env" 13 | ], 14 | "paths": { 15 | "@/*": [ 16 | "src/*" 17 | ] 18 | }, 19 | "lib": [ 20 | "esnext", 21 | "dom", 22 | "dom.iterable", 23 | "scripthost" 24 | ] 25 | }, 26 | "include": [ 27 | "src/**/*.ts", 28 | "src/**/*.tsx", 29 | "src/**/*.vue", 30 | "tests/**/*.ts", 31 | "tests/**/*.tsx" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": false, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | }, 14 | } --------------------------------------------------------------------------------