├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ ├── flow.svg │ └── useCase.svg ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── Routes.js ├── components │ ├── AddTrinoFavButton │ │ ├── AddTrinoFavButton.js │ │ └── AddTrinoFavButton.module.scss │ ├── AddTrinoForm │ │ ├── AddTrinoForm.js │ │ └── AddTrinoForm.module.scss │ ├── Layout │ │ ├── Layout.js │ │ └── Layout.module.scss │ ├── LoginForm │ │ ├── LoginForm.js │ │ └── LoginForm.module.scss │ ├── PrivateRoute │ │ └── PrivateRoute.js │ ├── RegisterForm │ │ ├── RegisterForm.js │ │ └── RegisterForm.module.scss │ └── TrinoList │ │ ├── TrinoList.js │ │ └── TrinoList.module.scss ├── contexts │ └── global.js ├── decorators │ ├── asyncInlineError │ │ └── index.js │ ├── index.js │ ├── inlineError │ │ └── index.js │ └── streamify │ │ └── index.js ├── domain │ ├── common │ │ ├── Entity.ts │ │ ├── Service.ts │ │ ├── UseCase.ts │ │ └── ValueObject.ts │ ├── index.ts │ ├── trino │ │ ├── Entities │ │ │ ├── TrinoEntity.ts │ │ │ └── factories.ts │ │ ├── Errors │ │ │ ├── NotFoundListTrinoError.ts │ │ │ ├── SomethingWrongTrinoError.ts │ │ │ ├── TrinoAggregateErrors.ts │ │ │ ├── TrinoError.ts │ │ │ └── factories.ts │ │ ├── Repositories │ │ │ ├── LocalStorageTrinoRepository.ts │ │ │ ├── TrinoRepository.ts │ │ │ └── factories.ts │ │ ├── UseCases │ │ │ ├── CreateTrinoUseCase.ts │ │ │ ├── ListTrinoUseCase.ts │ │ │ └── factories.ts │ │ └── ValueObjects │ │ │ ├── BodyValueObject.ts │ │ │ ├── TrinosListValueObject.ts │ │ │ └── factories.ts │ └── user │ │ ├── Entities │ │ ├── UserEntity.ts │ │ └── factories.ts │ │ ├── Repositories │ │ ├── InMemoryUserRepository.ts │ │ ├── LocalStorageUserRepository.ts │ │ ├── UserRepository.ts │ │ └── factories.ts │ │ ├── Services │ │ ├── CurrentUserService.ts │ │ └── factory.ts │ │ ├── UseCases │ │ ├── CurrentUserUseCase.ts │ │ ├── LoginUserUseCase.ts │ │ ├── LogoutUserUseCase.ts │ │ ├── RegisterUserUseCase.ts │ │ └── factories.ts │ │ └── ValueObjects │ │ ├── PasswordValueObject.ts │ │ ├── StatusValueObject.ts │ │ ├── UserNameValueObject.ts │ │ └── factories.ts ├── index.css ├── index.js ├── pages │ ├── Login │ │ ├── Login.js │ │ └── Login.module.scss │ ├── Register │ │ ├── Register.js │ │ └── Register.module.scss │ └── Trinos │ │ ├── Trinos.js │ │ └── Trinos.module.scss ├── react-app-env.d.ts ├── serviceWorker.js ├── setupTests.js ├── setupTests.ts ├── tests │ └── domain │ │ └── user │ │ └── CurrentUserUseCase.test.js └── theme.js ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["prettier", "@typescript-eslint"], 4 | "rules": { 5 | "no-unused-vars": "off", 6 | "@typescript-eslint/no-unused-vars": [ 7 | "error", 8 | { 9 | "args": "after-used", 10 | "argsIgnorePattern": "^_", 11 | "varsIgnorePattern": "^_" 12 | } 13 | ], 14 | "no-empty": ["error", { "allowEmptyCatch": true }], 15 | "no-var": "error", 16 | "no-console": ["warn", { "allow": ["warn", "error"] }], 17 | "prettier/prettier": "error", 18 | "@typescript-eslint/explicit-function-return-type": "off", 19 | "@typescript-eslint/explicit-module-boundary-types": "off", 20 | "@typescript-eslint/no-use-before-define": "off", 21 | "@typescript-eslint/no-var-requires": "off", 22 | "@typescript-eslint/camelcase": "off", 23 | "@typescript-eslint/ban-ts-ignore": "off", 24 | "react/prop-types": "off" 25 | }, 26 | "extends": [ 27 | "eslint:recommended", 28 | "plugin:react/recommended", 29 | "plugin:@typescript-eslint/eslint-recommended", 30 | "plugin:@typescript-eslint/recommended", 31 | "prettier/@typescript-eslint", 32 | "prettier" 33 | ], 34 | "parserOptions": { 35 | "ecmaVersion": 2020 36 | }, 37 | "env": { 38 | "es6": true, 39 | "node": true 40 | }, 41 | "globals": { 42 | "module": "writable", 43 | "process": "readonly", 44 | "exports": "readonly", 45 | "cy": "readonly" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.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 | 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Domain Driven Design strategies for the Frontend Architecture 2 | ------------------------------------------------------------- 3 | 4 | This repo contains an example of design strategies to build a clean frontend architecture. It uses a tweet clone as an example. 5 | 6 | ![flow](./public/assets/flow.svg) 7 | 8 | # The building blocks 9 | 10 | ## Use Case 11 | The verb of the architecture. To be used only to communicate with the exterior world. 12 | 13 | ![useCase](./public/assets/useCase.svg) 14 | 15 | ## Service 16 | The verb of the architecture. to be used only inside the domain. 17 | 18 | ## Entity Objects 19 | Are objects that has an entity. This objects are mutable. 20 | 21 | ## Value Objects 22 | Are objects that identify by his value. This objects should be inmutable. 23 | 24 | ## Agregates 25 | Are a group of objects considered as a unit regard to data changes. 26 | 27 | ## Factories 28 | Are used to encapsulate the knowledge neccesary for the object creation. 29 | 30 | ## Repositories 31 | Are used to encapsulate all the logic needed to obtain object references. Acts as a storage place for globally accesible objects. Can contain links to the infraestructure eg. the database. 32 | 33 | ## Modules 34 | Are a way to split the domain in managable parts. 35 | 36 | # Folder structure 37 | 38 | ``` 39 | ├── domain 40 | │   ├── index 41 | │   ├── common (context) 42 | │   │   ├── Entity 43 | │   │   ├── Service 44 | │   │   ├── UseCase 45 | │   │   └── ValueObject 46 | │   ├── trino (context) 47 | │   │   └── UseCases 48 | │   │   └── CreateTrinoUseCase 49 | │   └── user (context) 50 | │   ├── Entities 51 | │   │   ├── UserEntity 52 | │   │   └── factories 53 | │   ├── Repositories 54 | │   │   ├── InMemoryUserRepository 55 | │   │   ├── LocalStorageUserRepository 56 | │   │   ├── UserRepository 57 | │   │   └── factories 58 | │   ├── Services 59 | │   │   ├── CurrentUserService 60 | │   │   └── factory 61 | │   ├── UseCases 62 | │   │   ├── CurrentUserUseCase 63 | │   │   ├── LoginUserUseCase 64 | │   │   ├── LogoutUserUseCase 65 | │   │   ├── RegisterUserUseCase 66 | │   │   └── factories 67 | │   └── ValueObjects 68 | │   ├── PasswordValueObject 69 | │   ├── StatusValueObject 70 | │   ├── UserNameValueObject 71 | │   └── factories 72 | ``` 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pajarito-ts01", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.53", 12 | "@types/react-dom": "^16.9.8", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-scripts": "4.0.1", 16 | "typescript": "^4.0.3", 17 | "web-vitals": "^0.2.4", 18 | "@material-ui/core": "^4.11.0", 19 | "@material-ui/icons": "^4.9.1", 20 | "node-sass": "^4.14.1", 21 | "react-router-dom": "^5.2.0" 22 | }, 23 | "devDependencies": { 24 | "@typescript-eslint/eslint-plugin": "^2.34.0", 25 | "@typescript-eslint/parser": "^2.34.0", 26 | "eslint": "^7.2.0", 27 | "eslint-config-airbnb": "^18.2.1", 28 | "eslint-config-airbnb-typescript": "^12.0.0", 29 | "eslint-config-prettier": "^6.15.0", 30 | "eslint-plugin-import": "^2.22.1", 31 | "eslint-plugin-jest": "^24.1.0", 32 | "eslint-plugin-jsx-a11y": "^6.4.1", 33 | "eslint-plugin-prettier": "^3.1.4", 34 | "eslint-plugin-react": "^7.21.5", 35 | "eslint-plugin-react-hooks": "^4.0.0", 36 | "prettier": "^2.1.2" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject", 43 | "format": "prettier --write '**/*.{ts,tsx,js,jsx}'", 44 | "lint": "eslint . --ext .ts,.tsx" 45 | }, 46 | "eslintConfig": { 47 | "extends": [ 48 | "react-app", 49 | "react-app/jest" 50 | ] 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /public/assets/flow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Use Case
Use Case
Services
Services
Repositories
Repositories
Factories
Factories
 Encapsulated with 
 Encapsulated with 
 Accesed with 
 Accesed with 
 act as root of 
 act as root of 
Entity Objects
Entity Objects
 encapsulate width 
 encapsulate width 
Value Objects
Value Objects
Aggregates
Aggregates
outside world
outside world
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /public/assets/useCase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Services
Services
Objects
Objects
 calls 
 calls 
 returns 
 returns 
Use Case
Use Case
JSON
JSON
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosvillu/pajarito/dfc592d9dc3f488d8f6164a52460be397f6829f4/public/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosvillu/pajarito/dfc592d9dc3f488d8f6164a52460be397f6829f4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosvillu/pajarito/dfc592d9dc3f488d8f6164a52460be397f6829f4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom' 3 | import { Trinos } from './pages/Trinos/Trinos' 4 | import { Login } from './pages/Login/Login' 5 | import { Register } from './pages/Register/Register' 6 | import { PrivateRoute } from './components/PrivateRoute/PrivateRoute' 7 | 8 | export function Routes() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/AddTrinoFavButton/AddTrinoFavButton.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Fab from '@material-ui/core/Fab' 4 | import AddIcon from '@material-ui/icons/Add' 5 | import Dialog from '@material-ui/core/Dialog' 6 | import MuiDialogTitle from '@material-ui/core/DialogTitle' 7 | import IconButton from '@material-ui/core/IconButton' 8 | import Typography from '@material-ui/core/Typography' 9 | import CloseIcon from '@material-ui/icons/Close' 10 | import { AddTrinoForm } from '../AddTrinoForm/AddTrinoForm' 11 | import s from './AddTrinoFavButton.module.scss' 12 | 13 | export function AddTrinoFavButton({ user }) { 14 | const [open, setOpen] = useState(false) 15 | return ( 16 | <> 17 | setOpen(true)} 20 | className={s['add-trino-fav-button']} 21 | > 22 | 23 | 24 | 25 | setOpen(false)} 28 | disableBackdropClick 29 | disableEscapeKeyDown 30 | fullWidth 31 | maxWidth="sm" 32 | > 33 | 37 | Add a Trino 38 | setOpen(false)}> 39 | 40 | 41 | 42 | setOpen(false)} /> 43 | 44 | 45 | ) 46 | } 47 | 48 | AddTrinoFavButton.propTypes = { 49 | user: PropTypes.object, 50 | } 51 | -------------------------------------------------------------------------------- /src/components/AddTrinoFavButton/AddTrinoFavButton.module.scss: -------------------------------------------------------------------------------- 1 | .add-trino-fav-button { 2 | position: fixed !important; 3 | bottom: 1.5rem; 4 | right: 1.5rem; 5 | 6 | &__dialog-header{ 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/AddTrinoForm/AddTrinoForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react' 2 | import PropTypes from 'prop-types' 3 | import TextField from '@material-ui/core/TextField' 4 | import Button from '@material-ui/core/Button' 5 | import s from './AddTrinoForm.module.scss' 6 | 7 | import { Global } from '../../contexts/global' 8 | 9 | export function AddTrinoForm({ cb }) { 10 | const { domain } = useContext(Global) 11 | const [data, setData] = useState({}) 12 | 13 | async function onAddTrino(e) { 14 | e.preventDefault() 15 | const [error, trino] = await domain.get('createTrinoUseCase').execute(data) 16 | 17 | if (error) { 18 | return window.alert(error.message) 19 | } 20 | 21 | cb(trino) 22 | } 23 | 24 | function onChange(e) { 25 | const { name, value } = e.target 26 | 27 | setData({ ...data, [name]: value }) 28 | } 29 | 30 | return ( 31 |
32 | 40 | 41 |
42 | 45 |
46 | 47 | ) 48 | } 49 | 50 | AddTrinoForm.propTypes = { 51 | cb: PropTypes.func, 52 | } 53 | -------------------------------------------------------------------------------- /src/components/AddTrinoForm/AddTrinoForm.module.scss: -------------------------------------------------------------------------------- 1 | .add-trino-form { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 1.5rem; 5 | 6 | & > *:not(:last-child) { 7 | margin-bottom: 1rem; 8 | } 9 | 10 | &__actions { 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: flex-end; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import AppBar from '@material-ui/core/AppBar' 3 | import Toolbar from '@material-ui/core/Toolbar' 4 | import Typography from '@material-ui/core/Typography' 5 | import Button from '@material-ui/core/Button' 6 | import AccountCircle from '@material-ui/icons/AccountCircle' 7 | import Container from '@material-ui/core/Container' 8 | import PropTypes from 'prop-types' 9 | import s from './Layout.module.scss' 10 | import { useHistory } from 'react-router-dom' 11 | 12 | import { Global } from '../../contexts/global' 13 | 14 | export function Layout({ name, userName, children }) { 15 | const { domain } = useContext(Global) 16 | const history = useHistory() 17 | 18 | async function handleLogout() { 19 | const [error] = await domain.get('logoutUserUseCase').execute() 20 | 21 | if (error) { 22 | return window.alert(error.message) 23 | } 24 | history.push('/login') 25 | } 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | {name} 34 | 35 | 36 | 39 | 40 | 41 | 42 |
{children}
43 |
44 | ) 45 | } 46 | 47 | Layout.propTypes = { 48 | name: PropTypes.string, 49 | userName: PropTypes.string, 50 | children: PropTypes.oneOfType([ 51 | PropTypes.arrayOf(PropTypes.element), 52 | PropTypes.element, 53 | ]), 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | &__toolbar { 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | 11 | &__content { 12 | display: flex; 13 | flex-direction: column; 14 | flex-grow: 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react' 2 | import Paper from '@material-ui/core/Paper' 3 | import s from './LoginForm.module.scss' 4 | import TextField from '@material-ui/core/TextField' 5 | import Typography from '@material-ui/core/Typography' 6 | import Button from '@material-ui/core/Button' 7 | import { Link, useHistory } from 'react-router-dom' 8 | import { Global } from '../../contexts/global' 9 | 10 | export function LoginForm() { 11 | const { domain } = useContext(Global) 12 | const [data, setData] = useState({}) 13 | const history = useHistory() 14 | 15 | useEffect(() => { 16 | domain 17 | .get('currentUserUseCase') 18 | .execute() 19 | .then(([error, user]) => { 20 | if (error) { 21 | console.log(error) // eslint-disable-line no-console 22 | return null 23 | } 24 | user && history.push('/') 25 | }) 26 | }, [domain, history]) 27 | 28 | async function onLogin(e) { 29 | e.preventDefault() 30 | const [error] = await domain.get('loginUserUseCase').execute(data) 31 | if (error) return console.log(error) // eslint-disable-line no-console 32 | history.push('/') 33 | } 34 | 35 | function onChange(e) { 36 | const { name, value } = e.target 37 | 38 | setData({ ...data, [name]: value }) 39 | } 40 | 41 | return ( 42 | 43 | 44 | Login 45 | 46 |
47 | 53 | 60 | 61 |
62 | 65 | 68 |
69 | 70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/LoginForm/LoginForm.module.scss: -------------------------------------------------------------------------------- 1 | .login-form { 2 | padding: 2rem; 3 | 4 | &__form { 5 | display: flex; 6 | flex-direction: column; 7 | margin-top: 1rem; 8 | 9 | & > *:not(:last-child) { 10 | margin-bottom: 1rem; 11 | } 12 | } 13 | 14 | &__actions { 15 | display: flex; 16 | justify-content: flex-end; 17 | flex-direction: row; 18 | 19 | & > *:not(:last-child) { 20 | margin-right: 1rem; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/PrivateRoute/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // import React, {useState, useEffect, useContext} from 'react' 3 | import { Route, Redirect } from 'react-router-dom' 4 | import PropTypes from 'prop-types' 5 | 6 | // import {Global} from '../../contexts/global' 7 | const EMPTY_DB = JSON.stringify({}) 8 | const USERS_KEY = 'users' 9 | const CURRENT_USER_KEY = '__CURRENT_USER__' 10 | 11 | export function PrivateRoute({ component: Component, ...rest }) { 12 | // const {domain} = useContext(Global) 13 | // const [user, setUser] = useState(null) 14 | 15 | // can't get this work with domain 16 | const usersJSON = window.localStorage.getItem(USERS_KEY) || EMPTY_DB 17 | const usersDB = JSON.parse(usersJSON) 18 | const user = usersDB[CURRENT_USER_KEY] 19 | 20 | // // needs login on each refresh 21 | // useEffect(() => { 22 | // async function fn() { 23 | // const [error, data] = await domain.get('currentUserUseCase').execute() 24 | // if (error) return console.log(error) 25 | // 26 | // setUser(data) 27 | // } 28 | // 29 | // fn() 30 | // }, [domain]) 31 | 32 | if (user) console.log(user) 33 | if (!user) console.log('not user') 34 | 35 | return ( 36 | 39 | user ? ( 40 | 41 | ) : ( 42 | 48 | ) 49 | } 50 | /> 51 | ) 52 | } 53 | 54 | PrivateRoute.propTypes = { 55 | component: PropTypes.elementType, 56 | } 57 | -------------------------------------------------------------------------------- /src/components/RegisterForm/RegisterForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react' 2 | import Paper from '@material-ui/core/Paper' 3 | import s from './RegisterForm.module.scss' 4 | import TextField from '@material-ui/core/TextField' 5 | import Typography from '@material-ui/core/Typography' 6 | import Button from '@material-ui/core/Button' 7 | import { Link, useHistory } from 'react-router-dom' 8 | 9 | import { Global } from '../../contexts/global' 10 | 11 | export function RegisterForm() { 12 | const { domain } = useContext(Global) 13 | const [data, setData] = useState({}) 14 | const history = useHistory() 15 | 16 | async function onRegister(e) { 17 | e.preventDefault() 18 | const [error] = await domain.get('registerUserUseCase').execute(data) 19 | 20 | if (error) { 21 | return window.alert(error.message) 22 | } 23 | history.push('/login') 24 | } 25 | 26 | function onChange(e) { 27 | const { name, value } = e.target 28 | 29 | setData({ ...data, [name]: value }) 30 | } 31 | 32 | return ( 33 | 34 | 35 | Register 36 | 37 |
42 | 48 | 55 | 56 |
57 | 60 | 63 |
64 | 65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/RegisterForm/RegisterForm.module.scss: -------------------------------------------------------------------------------- 1 | .register-form { 2 | padding: 2rem; 3 | 4 | &__form { 5 | display: flex; 6 | flex-direction: column; 7 | margin-top: 1rem; 8 | 9 | & > *:not(:last-child) { 10 | margin-bottom: 1rem; 11 | } 12 | } 13 | 14 | &__actions { 15 | display: flex; 16 | justify-content: flex-end; 17 | flex-direction: row; 18 | 19 | & > *:not(:last-child) { 20 | margin-right: 1rem; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/TrinoList/TrinoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import s from './TrinoList.module.scss' 3 | import Paper from '@material-ui/core/Paper' 4 | import Typography from '@material-ui/core/Typography' 5 | import PropTypes from 'prop-types' 6 | 7 | export function TrinoList({ trinos }) { 8 | return ( 9 |
10 | {trinos.map((trino) => ( 11 | 12 | 13 | {trino.body.body} 14 | 15 | 16 | ))} 17 |
18 | ) 19 | } 20 | 21 | TrinoList.propTypes = { 22 | trinos: PropTypes.array, 23 | } 24 | -------------------------------------------------------------------------------- /src/components/TrinoList/TrinoList.module.scss: -------------------------------------------------------------------------------- 1 | .trino-list { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | &__trino { 6 | padding: 1rem; 7 | margin-bottom: 1rem; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/contexts/global.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export const Global = createContext() 4 | -------------------------------------------------------------------------------- /src/decorators/asyncInlineError/index.js: -------------------------------------------------------------------------------- 1 | const _runner = ({ instance, original } = {}) => { 2 | return function (...args) { 3 | const response = [] 4 | Object.defineProperty(response, '__INLINE_ERROR__', { 5 | enumerable: false, 6 | writable: true, 7 | value: true, 8 | }) 9 | try { 10 | const returns = original.apply( 11 | instance.__STREAMIFY__ ? this : instance, 12 | args 13 | ) 14 | return returns 15 | .then((r) => { 16 | response[0] = null 17 | response[1] = r 18 | return response 19 | }) 20 | .catch((e) => { 21 | response[0] = e 22 | response[1] = null 23 | return Promise.resolve(response) 24 | }) 25 | } catch (e) { 26 | response[0] = e 27 | response[1] = null 28 | return response 29 | } 30 | } 31 | } 32 | 33 | export const asyncInlineError = () => (target, fnName, descriptor) => { 34 | const { value: fn, configurable, enumerable } = descriptor 35 | 36 | // https://github.com/jayphelps/core-decorators.js/blob/master/src/autobind.js 37 | return Object.assign( 38 | {}, 39 | { 40 | configurable, 41 | enumerable, 42 | get() { 43 | const _fnRunner = _runner({ 44 | instance: this, 45 | original: fn, 46 | }) 47 | 48 | if (this === target && !target.__STREAMIFY__) { 49 | return fn 50 | } 51 | 52 | Object.defineProperty(this, fnName, { 53 | configurable: true, 54 | writable: true, 55 | enumerable: false, 56 | value: _fnRunner, 57 | }) 58 | return _fnRunner 59 | }, 60 | set(newValue) { 61 | Object.defineProperty(this, fnName, { 62 | configurable: true, 63 | writable: true, 64 | enumerable: true, 65 | value: newValue, 66 | }) 67 | 68 | return newValue 69 | }, 70 | } 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/decorators/index.js: -------------------------------------------------------------------------------- 1 | export { asyncInlineError } from './asyncInlineError' 2 | export { streamify } from './streamify' 3 | -------------------------------------------------------------------------------- /src/decorators/inlineError/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosvillu/pajarito/dfc592d9dc3f488d8f6164a52460be397f6829f4/src/decorators/inlineError/index.js -------------------------------------------------------------------------------- /src/decorators/streamify/index.js: -------------------------------------------------------------------------------- 1 | const isPromise = (obj) => 2 | typeof obj !== 'undefined' && typeof obj.then === 'function' 3 | 4 | const defaultErrorHandler = (err) => { 5 | console.error(err) 6 | } 7 | 8 | const dispatchToListeners = ({ onError, onNext, params, result }) => { 9 | if (isPromise(result)) { 10 | result 11 | .then((value) => { 12 | if (value.__INLINE_ERROR__) { 13 | const [error, val] = value 14 | return !error 15 | ? onNext({ params, result: val }) 16 | : onError({ params, error }) 17 | } 18 | onNext({ params, result: value }) 19 | }) 20 | .catch((error) => onError({ params, error })) 21 | } else if (result) { 22 | if (result.__INLINE_ERROR__) { 23 | const [error, val] = result 24 | return !error 25 | ? onNext({ params, result: val }) 26 | : onError({ params, error }) 27 | } 28 | onNext({ params, result }) 29 | } 30 | } 31 | 32 | const createSubscription = (proto, method, originalMethod) => { 33 | let onNextListeners = [] 34 | let onErrorListeners = [] 35 | 36 | proto[method] = function (...args) { 37 | const params = args 38 | try { 39 | const result = originalMethod.apply(this, args) 40 | dispatchToListeners({ onError, onNext, params, result }) 41 | return result 42 | } catch (error) { 43 | onError({ params, error }) 44 | throw error 45 | } 46 | } 47 | 48 | const onNext = ({ params, result }) => { 49 | onNextListeners.forEach((listener) => listener({ params, result })) 50 | } 51 | 52 | const onError = ({ params, error }) => { 53 | onErrorListeners.forEach((listener) => listener({ params, error })) 54 | } 55 | 56 | // eslint-disable-next-line @typescript-eslint/no-empty-function 57 | const subscribe = (onNext = () => {}, onError = defaultErrorHandler) => { 58 | onNextListeners.push(onNext) 59 | onErrorListeners.push(onError) 60 | 61 | return { 62 | dispose: () => { 63 | onNextListeners = onNextListeners.filter((l) => l !== onNext) 64 | onErrorListeners = onErrorListeners.filter((l) => l !== onError) 65 | }, 66 | } 67 | } 68 | 69 | return { subscribe } 70 | } 71 | 72 | const reducer = (Target, proto, method) => { 73 | const originalMethod = Target.prototype[method] 74 | proto.$ = proto.$ || {} 75 | proto.$[method] = createSubscription(proto, method, originalMethod) 76 | return proto 77 | } 78 | 79 | export const streamify = (...methods) => { 80 | return (Target) => { 81 | Target.prototype.__STREAMIFY__ = true 82 | Object.assign( 83 | Target.prototype, 84 | methods 85 | .filter((method) => !!Target.prototype[method]) 86 | .reduce(reducer.bind(null, Target), {}) 87 | ) 88 | return Target 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/domain/common/Entity.ts: -------------------------------------------------------------------------------- 1 | export class Entity { 2 | toJSON() { 3 | throw new Error('[Entity#toJSON] should be implemented') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/common/Service.ts: -------------------------------------------------------------------------------- 1 | export class Service { 2 | execute() { 3 | throw new Error('[Service#execute] should be implemented') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/common/UseCase.ts: -------------------------------------------------------------------------------- 1 | export class UseCase { 2 | execute(_param) { 3 | throw new Error('[UseCase#execute] should be implemented') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/common/ValueObject.ts: -------------------------------------------------------------------------------- 1 | export class ValueObject { 2 | toJSON() { 3 | throw new Error('[ValueObject#toJSON] should be implemented') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/domain/index.ts: -------------------------------------------------------------------------------- 1 | import { TrinoUseCasesFactory } from './trino/UseCases/factories' 2 | 3 | import { UserUseCasesFactory } from './user/UseCases/factories' 4 | 5 | const USE_CASES = { 6 | listTrinoUseCase: TrinoUseCasesFactory.listTrinoUseCase(), 7 | createTrinoUseCase: TrinoUseCasesFactory.createTrinoUseCase(), 8 | 9 | currentUserUseCase: UserUseCasesFactory.currentUserUseCase(), 10 | logoutUserUseCase: UserUseCasesFactory.logoutUserUseCase(), 11 | loginUserUseCase: UserUseCasesFactory.loginUserUseCase(), 12 | registerUserUseCase: UserUseCasesFactory.registerUserUseCase(), 13 | } 14 | 15 | export class Pajarito { 16 | get(key) { 17 | if (!USE_CASES[key]) { 18 | throw new Error(`[Pajarito#get] key(${key}) NOT FOUND`) 19 | } 20 | 21 | return USE_CASES[key] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/domain/trino/Entities/TrinoEntity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../common/Entity' 2 | 3 | import { UserEntity } from '../../user/Entities/UserEntity' 4 | import { BodyValueObject } from '../ValueObjects/BodyValueObject' 5 | 6 | export class TrinoEntity extends Entity { 7 | #timestamp 8 | #body 9 | #id 10 | #user 11 | 12 | static generateUUID() { 13 | return ( 14 | Math.random().toString(36).substring(2, 15) + 15 | Math.random().toString(36).substring(2, 15) 16 | ) 17 | } 18 | 19 | static validate({ body, id, user, timestamp }) { 20 | if (!body || !id || !user || !timestamp) { 21 | throw new Error( 22 | `[TrinoEntity.validate] forbidden TrinoEntity body(${body}) id(${id}) user(${user}) timestamp(${timestamp})` 23 | ) 24 | } 25 | 26 | if (!(body instanceof BodyValueObject)) { 27 | throw new Error( 28 | `[TrinoEntity.validate] body is not instanceof BodyValueObject body(${body})` 29 | ) 30 | } 31 | 32 | if (!(user instanceof UserEntity)) { 33 | throw new Error( 34 | `[TrinoEntity.validate] user is not instanceof UserEntity user(${user})` 35 | ) 36 | } 37 | } 38 | 39 | constructor({ body, id, user, timestamp }) { 40 | super() 41 | this.#timestamp = timestamp 42 | this.#id = id 43 | this.#body = body 44 | this.#user = user 45 | } 46 | 47 | toJSON() { 48 | return { 49 | id: this.#id, 50 | timestamp: this.#timestamp, 51 | body: this.#body.toJSON(), 52 | user: this.#user.toJSON(), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/domain/trino/Entities/factories.ts: -------------------------------------------------------------------------------- 1 | import { UserEntitiesFactory } from '../../user/Entities/factories' 2 | import { TrinoValueObjectsFactory } from '../ValueObjects/factories' 3 | 4 | import { TrinoEntity } from './TrinoEntity' 5 | 6 | export class TrinoEntitiesFactory { 7 | static trinoEntity({ body, id, user, timestamp }) { 8 | const bodyValueObject = TrinoValueObjectsFactory.bodyValueObject(body) 9 | const userEntity = UserEntitiesFactory.userEntity(user) 10 | 11 | TrinoEntity.validate({ 12 | id, 13 | timestamp, 14 | body: bodyValueObject, 15 | user: userEntity, 16 | }) 17 | 18 | return new TrinoEntity({ 19 | body: bodyValueObject, 20 | id, 21 | user: userEntity, 22 | timestamp, 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/domain/trino/Errors/NotFoundListTrinoError.ts: -------------------------------------------------------------------------------- 1 | import { TrinoError } from './TrinoError' 2 | 3 | export class NotFoundListTrinoError extends TrinoError { 4 | constructor() { 5 | super('[NotFoundListTrinoError] List unavailable') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/trino/Errors/SomethingWrongTrinoError.ts: -------------------------------------------------------------------------------- 1 | import { TrinoError } from './TrinoError' 2 | 3 | export class SomethingWrongTrinoError extends TrinoError { 4 | constructor() { 5 | super('[SomethingWrongTrinoError] TODO MAL!') 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/domain/trino/Errors/TrinoAggregateErrors.ts: -------------------------------------------------------------------------------- 1 | export class TrinoAggregateError extends window.AggregateError {} 2 | -------------------------------------------------------------------------------- /src/domain/trino/Errors/TrinoError.ts: -------------------------------------------------------------------------------- 1 | export class TrinoError extends Error {} 2 | -------------------------------------------------------------------------------- /src/domain/trino/Errors/factories.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundListTrinoError } from './NotFoundListTrinoError' 2 | import { SomethingWrongTrinoError } from './SomethingWrongTrinoError' 3 | 4 | export class TrinoErrorsFactory { 5 | static notFoundListTrinoError() { 6 | return new NotFoundListTrinoError() 7 | } 8 | 9 | static somethingWrongTrinoError() { 10 | return new SomethingWrongTrinoError() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/domain/trino/Repositories/LocalStorageTrinoRepository.ts: -------------------------------------------------------------------------------- 1 | import { asyncInlineError } from '../../../decorators/asyncInlineError' 2 | 3 | import { TrinoRepository } from './TrinoRepository' 4 | import { TrinoEntity } from '../Entities/TrinoEntity' 5 | 6 | const EMPTY_DB = JSON.stringify({ trinos: [] }) 7 | const TRINOS_KEY = 'trinos' 8 | 9 | export class LocalStorageTrinoRepository extends TrinoRepository { 10 | #notFoundListTrinoErrorFactory 11 | #trinosListValueFactory 12 | #trinoEntityFactory 13 | #localStorage 14 | 15 | constructor({ 16 | localStorage, 17 | trinoEntityFactory, 18 | trinosListValueFactory, 19 | notFoundListTrinoErrorFactory, 20 | }) { 21 | super() 22 | this.#localStorage = localStorage 23 | this.#trinoEntityFactory = trinoEntityFactory 24 | this.#trinosListValueFactory = trinosListValueFactory 25 | this.#notFoundListTrinoErrorFactory = notFoundListTrinoErrorFactory 26 | } 27 | 28 | @asyncInlineError() 29 | async all() { 30 | const trinosJSON = this.#localStorage.getItem(TRINOS_KEY) || EMPTY_DB 31 | const trinosDB = JSON.parse(trinosJSON) 32 | 33 | if (!trinosDB.trinos) { 34 | throw this.#notFoundListTrinoErrorFactory() 35 | } 36 | 37 | const trinos = trinosDB.trinos.map(this.#trinoEntityFactory) 38 | 39 | return this.#trinosListValueFactory({ 40 | trinos: trinos.map((trino) => trino.toJSON()), 41 | }) 42 | } 43 | 44 | async create({ body, user }) { 45 | const id = TrinoEntity.generateUUID() 46 | const trinosJSON = this.#localStorage.getItem(TRINOS_KEY) || EMPTY_DB 47 | const trinosDB = JSON.parse(trinosJSON) 48 | 49 | const newTrino = this.#trinoEntityFactory({ 50 | id, 51 | timestamp: Date.now(), 52 | user: user.toJSON(), 53 | body: body.toJSON(), 54 | }) 55 | 56 | const nextTrinosDB = { 57 | trinos: [newTrino.toJSON(), ...trinosDB.trinos], 58 | } 59 | 60 | this.#localStorage.setItem(TRINOS_KEY, JSON.stringify(nextTrinosDB)) 61 | 62 | return newTrino 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/domain/trino/Repositories/TrinoRepository.ts: -------------------------------------------------------------------------------- 1 | export class TrinoRepository { 2 | create(_param) { 3 | throw new Error('[TrinoRepository#create] should be implemented') 4 | } 5 | 6 | all() { 7 | throw new Error('[TrinoRepository#all] should be implemented') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/trino/Repositories/factories.ts: -------------------------------------------------------------------------------- 1 | import { TrinoErrorsFactory } from '../Errors/factories' 2 | import { TrinoValueObjectsFactory } from '../ValueObjects/factories' 3 | import { TrinoEntitiesFactory } from '../Entities/factories' 4 | 5 | import { LocalStorageTrinoRepository } from './LocalStorageTrinoRepository' 6 | 7 | export class TrinoRepositoriesFactory { 8 | static localStorageTrinoRepository() { 9 | return new LocalStorageTrinoRepository({ 10 | notFoundListTrinoErrorFactory: TrinoErrorsFactory.notFoundListTrinoError, 11 | trinosListValueFactory: TrinoValueObjectsFactory.trinosListValueObject, 12 | trinoEntityFactory: TrinoEntitiesFactory.trinoEntity, 13 | localStorage: window.localStorage, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/domain/trino/UseCases/CreateTrinoUseCase.ts: -------------------------------------------------------------------------------- 1 | import { streamify, asyncInlineError } from '../../../decorators' 2 | import { UseCase } from '../../common/UseCase' 3 | 4 | @streamify('execute') 5 | class CreateTrinoUseCase extends UseCase { 6 | #repository 7 | #bodyValueObjectFactory 8 | #currentUserService 9 | 10 | constructor({ repository, currentUserService, bodyValueObjectFactory }) { 11 | super() 12 | 13 | this.#repository = repository 14 | this.#bodyValueObjectFactory = bodyValueObjectFactory 15 | this.#currentUserService = currentUserService 16 | } 17 | 18 | @asyncInlineError() 19 | async execute({ body: intro }) { 20 | const body = this.#bodyValueObjectFactory({ body: intro }) 21 | const currentUser = await this.#currentUserService.execute() 22 | 23 | const trino = await this.#repository.create({ 24 | body, 25 | user: currentUser, 26 | }) 27 | 28 | return trino.toJSON() 29 | } 30 | } 31 | 32 | export { CreateTrinoUseCase } 33 | -------------------------------------------------------------------------------- /src/domain/trino/UseCases/ListTrinoUseCase.ts: -------------------------------------------------------------------------------- 1 | import { streamify, asyncInlineError } from '../../../decorators' 2 | import { UseCase } from '../../common/UseCase' 3 | 4 | @streamify('execute') 5 | class ListTrinoUseCase extends UseCase { 6 | #somethingWrongTrinoErrorFactory 7 | #repository 8 | 9 | constructor({ repository, somethingWrongTrinoErrorFactory }) { 10 | super() 11 | this.#repository = repository 12 | this.#somethingWrongTrinoErrorFactory = somethingWrongTrinoErrorFactory 13 | } 14 | 15 | @asyncInlineError() 16 | async execute() { 17 | const [error, TrinosList] = await this.#repository.all() 18 | 19 | if (error) { 20 | throw error 21 | } 22 | 23 | return TrinosList.toJSON() 24 | } 25 | } 26 | 27 | export { ListTrinoUseCase } 28 | -------------------------------------------------------------------------------- /src/domain/trino/UseCases/factories.ts: -------------------------------------------------------------------------------- 1 | import { UserServicesFactory } from '../../user/Services/factory' 2 | 3 | import { TrinoErrorsFactory } from '../Errors/factories' 4 | import { TrinoValueObjectsFactory } from '../ValueObjects/factories' 5 | import { TrinoRepositoriesFactory } from '../Repositories/factories' 6 | 7 | import { ListTrinoUseCase } from './ListTrinoUseCase' 8 | import { CreateTrinoUseCase } from './CreateTrinoUseCase' 9 | 10 | export class TrinoUseCasesFactory { 11 | static listTrinoUseCase() { 12 | return new ListTrinoUseCase({ 13 | somethingWrongTrinoErrorFactory: 14 | TrinoErrorsFactory.somethingWrongTrinoError, 15 | repository: TrinoRepositoriesFactory.localStorageTrinoRepository(), 16 | }) 17 | } 18 | 19 | static createTrinoUseCase() { 20 | return new CreateTrinoUseCase({ 21 | repository: TrinoRepositoriesFactory.localStorageTrinoRepository(), 22 | currentUserService: UserServicesFactory.currentUserService(), 23 | bodyValueObjectFactory: TrinoValueObjectsFactory.bodyValueObject, 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/domain/trino/ValueObjects/BodyValueObject.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '../../common/ValueObject' 2 | 3 | export class BodyValueObject extends ValueObject { 4 | #body 5 | 6 | static validate({ body }) { 7 | if (!body) { 8 | throw new Error('[BodyValueObject.validate] forbidden empty bodies') 9 | } 10 | } 11 | 12 | constructor({ body }) { 13 | super() 14 | this.#body = body 15 | } 16 | 17 | toJSON() { 18 | return { 19 | body: this.#body, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/domain/trino/ValueObjects/TrinosListValueObject.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '../../common/ValueObject' 2 | 3 | export class TrinosListValueObject extends ValueObject { 4 | #trinos 5 | 6 | static validate({ trinos }) { 7 | if (!Array.isArray(trinos)) { 8 | throw new Error( 9 | `[TrinosListValueObject.validate] trinos should be instanceof Array trinos(${trinos})` 10 | ) 11 | } 12 | } 13 | 14 | constructor({ trinos }) { 15 | super() 16 | this.#trinos = trinos 17 | } 18 | 19 | toJSON() { 20 | return { trinos: this.#trinos.map((trino) => trino.toJSON()) } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/domain/trino/ValueObjects/factories.ts: -------------------------------------------------------------------------------- 1 | import { TrinoEntitiesFactory } from '../Entities/factories' 2 | 3 | import { BodyValueObject } from './BodyValueObject' 4 | import { TrinosListValueObject } from './TrinosListValueObject' 5 | 6 | export class TrinoValueObjectsFactory { 7 | static bodyValueObject({ body }) { 8 | BodyValueObject.validate({ body }) 9 | return new BodyValueObject({ body }) 10 | } 11 | 12 | static trinosListValueObject({ trinos }) { 13 | const trinosEntities = trinos.map(TrinoEntitiesFactory.trinoEntity) 14 | 15 | TrinosListValueObject.validate({ trinos: trinosEntities }) 16 | 17 | return new TrinosListValueObject({ trinos: trinosEntities }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/domain/user/Entities/UserEntity.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../common/Entity' 2 | 3 | interface User { 4 | username: string 5 | id: string 6 | } 7 | 8 | export class UserEntity extends Entity { 9 | #username 10 | #id 11 | 12 | static generateUUID() { 13 | let dt = new Date().getTime() 14 | const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( 15 | /[xy]/g, 16 | function (c) { 17 | const r = (dt + Math.random() * 16) % 16 | 0 18 | dt = Math.floor(dt / 16) 19 | return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) 20 | } 21 | ) 22 | return uuid 23 | } 24 | 25 | static validate({ id, username }: User) { 26 | if (!username || !id) { 27 | throw new Error( 28 | `[UserEntity.validate] forbidden UserEntity username(${username}) id(${id})` 29 | ) 30 | } 31 | } 32 | 33 | constructor({ id, username }: User) { 34 | super() 35 | this.#username = username 36 | this.#id = id 37 | } 38 | 39 | toJSON(): User { 40 | return { 41 | username: this.#username, 42 | id: this.#id, 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/domain/user/Entities/factories.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './UserEntity' 2 | 3 | export class UserEntitiesFactory { 4 | static userEntity({ id, username }) { 5 | UserEntity.validate({ id, username }) 6 | 7 | return new UserEntity({ id, username }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/domain/user/Repositories/InMemoryUserRepository.ts: -------------------------------------------------------------------------------- 1 | import { UserRepository } from './UserRepository' 2 | 3 | export class InMemoryUserRepository extends UserRepository {} 4 | -------------------------------------------------------------------------------- /src/domain/user/Repositories/LocalStorageUserRepository.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from '../Entities/UserEntity' 2 | import { UserRepository } from './UserRepository' 3 | 4 | const EMPTY_DB = JSON.stringify({}) 5 | const USERS_KEY = 'users' 6 | const CURRENT_USER_KEY = '__CURRENT_USER__' 7 | 8 | interface UserData { 9 | username: string 10 | id: string 11 | password: string 12 | } 13 | 14 | export class LocalStorageUserRepository extends UserRepository { 15 | #statusValueObjectFactory 16 | #userEntityFactory 17 | #localStorage 18 | 19 | constructor({ userEntityFactory, statusValueObjectFactory, localStorage }) { 20 | super() 21 | 22 | this.#statusValueObjectFactory = statusValueObjectFactory 23 | this.#userEntityFactory = userEntityFactory 24 | this.#localStorage = localStorage 25 | } 26 | 27 | async current() { 28 | const usersJSON = this.#localStorage.getItem(USERS_KEY) || EMPTY_DB 29 | const usersDB = JSON.parse(usersJSON) 30 | 31 | const currentUserJSON = usersDB[CURRENT_USER_KEY] 32 | 33 | if (!currentUserJSON) { 34 | throw new Error('[UserRepository#current] user NOT FOUND') 35 | } 36 | 37 | return this.#userEntityFactory(currentUserJSON) 38 | } 39 | 40 | async logout() { 41 | const usersJSON = this.#localStorage.getItem(USERS_KEY) || EMPTY_DB 42 | const usersDB = JSON.parse(usersJSON) 43 | 44 | const nextUsersDB = Object.assign(usersDB, { 45 | [CURRENT_USER_KEY]: undefined, 46 | }) 47 | 48 | this.#localStorage.setItem(USERS_KEY, JSON.stringify(nextUsersDB)) 49 | 50 | return this.#statusValueObjectFactory({ status: true }) 51 | } 52 | 53 | async login({ username, password }) { 54 | const usersJSON = this.#localStorage.getItem(USERS_KEY) || EMPTY_DB 55 | const usersDB: UserData[] = JSON.parse(usersJSON) 56 | 57 | const userJSON: UserData = Object.values(usersDB).find( 58 | (user) => user.username === username.value() 59 | ) 60 | 61 | if (!usersJSON || userJSON.password !== password.value()) { 62 | throw new Error('[UserRepository#login] forbidden user') 63 | } 64 | 65 | const nextUsersDB = Object.assign(usersDB, { 66 | [CURRENT_USER_KEY]: { 67 | ...userJSON, 68 | password: undefined, 69 | }, 70 | }) 71 | 72 | this.#localStorage.setItem(USERS_KEY, JSON.stringify(nextUsersDB)) 73 | 74 | return this.#userEntityFactory(userJSON) 75 | } 76 | 77 | async register({ username, password }) { 78 | const newUserID = UserEntity.generateUUID() 79 | const usersJSON = this.#localStorage.getItem(USERS_KEY) || EMPTY_DB 80 | const usersDB: UserData = JSON.parse(usersJSON) 81 | 82 | /** 83 | * 84 | * const usersDB = { 85 | * [id]: { 86 | * id, 87 | * username, 88 | * password 89 | * } 90 | * } 91 | * 92 | * */ 93 | 94 | if ( 95 | Object.values(usersDB).some((user) => user.username === username.value()) 96 | ) { 97 | throw new Error( 98 | '[UserRepository#register] forbidden register already used username' 99 | ) 100 | } 101 | 102 | const nextUsersDB = Object.assign(usersDB, { 103 | [newUserID]: { 104 | id: newUserID, 105 | username: username.value(), 106 | password: password.value(), 107 | }, 108 | }) 109 | 110 | this.#localStorage.setItem(USERS_KEY, JSON.stringify(nextUsersDB)) 111 | 112 | return this.#userEntityFactory({ 113 | id: newUserID, 114 | username: username.value(), 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/domain/user/Repositories/UserRepository.ts: -------------------------------------------------------------------------------- 1 | export class UserRepository { 2 | register(_param) { 3 | throw new Error('[UserRepository#register] should be implemented') 4 | } 5 | 6 | login(_param) { 7 | throw new Error('[UserRepository#login] should be implemented') 8 | } 9 | 10 | logout() { 11 | throw new Error('[UserRepository#logout] should be implemented') 12 | } 13 | 14 | current() { 15 | throw new Error('[UserRepository#current] should be implemented') 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/domain/user/Repositories/factories.ts: -------------------------------------------------------------------------------- 1 | import { UserEntitiesFactory } from '../Entities/factories' 2 | import { UserValueObjectsFactory } from '../ValueObjects/factories' 3 | 4 | import { LocalStorageUserRepository } from './LocalStorageUserRepository' 5 | import { InMemoryUserRepository } from './InMemoryUserRepository' 6 | 7 | export class UserRepositoriesFactory { 8 | static inMemoryUserRepository() { 9 | return new InMemoryUserRepository() 10 | } 11 | 12 | static localStorageUserRepository() { 13 | return new LocalStorageUserRepository({ 14 | userEntityFactory: UserEntitiesFactory.userEntity, 15 | statusValueObjectFactory: UserValueObjectsFactory.statusValueObject, 16 | localStorage: window.localStorage, 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/domain/user/Services/CurrentUserService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../../common/Service' 2 | 3 | export class CurrentUserService extends Service { 4 | #repository 5 | 6 | constructor({ repository }) { 7 | super() 8 | 9 | this.#repository = repository 10 | } 11 | 12 | async execute() { 13 | const user = await this.#repository.current() 14 | 15 | return user 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/domain/user/Services/factory.ts: -------------------------------------------------------------------------------- 1 | import { UserRepositoriesFactory } from '../Repositories/factories' 2 | import { CurrentUserService } from './CurrentUserService' 3 | 4 | const isNODE = typeof window === 'undefined' 5 | 6 | export class UserServicesFactory { 7 | static currentUserService() { 8 | return new CurrentUserService({ 9 | repository: isNODE 10 | ? UserRepositoriesFactory.inMemoryUserRepository() 11 | : UserRepositoriesFactory.localStorageUserRepository(), 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/domain/user/UseCases/CurrentUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { streamify, asyncInlineError } from '../../../decorators' 2 | import { UseCase } from '../../common/UseCase' 3 | 4 | @streamify('execute') 5 | class CurrentUserUseCase extends UseCase { 6 | #service 7 | 8 | constructor({ service }) { 9 | super() 10 | 11 | this.#service = service 12 | } 13 | 14 | @asyncInlineError() 15 | async execute() { 16 | const user = await this.#service.execute() 17 | 18 | return user.toJSON() 19 | } 20 | } 21 | 22 | export { CurrentUserUseCase } 23 | -------------------------------------------------------------------------------- /src/domain/user/UseCases/LoginUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { streamify, asyncInlineError } from '../../../decorators' 2 | import { UseCase } from '../../common/UseCase' 3 | 4 | @streamify('execute') 5 | class LoginUserUseCase extends UseCase { 6 | #repository 7 | #usernameValueObjectFactory 8 | #passwordValueObjectFactory 9 | 10 | constructor({ 11 | repository, 12 | usernameValueObjectFactory, 13 | passwordValueObjectFactory, 14 | }) { 15 | super() 16 | this.#repository = repository 17 | this.#usernameValueObjectFactory = usernameValueObjectFactory 18 | this.#passwordValueObjectFactory = passwordValueObjectFactory 19 | } 20 | 21 | @asyncInlineError() 22 | async execute({ username, password }) { 23 | const user = await this.#repository.login({ 24 | username: this.#usernameValueObjectFactory({ username }), 25 | password: this.#passwordValueObjectFactory({ password }), 26 | }) 27 | 28 | return user.toJSON() 29 | } 30 | } 31 | 32 | export { LoginUserUseCase } 33 | -------------------------------------------------------------------------------- /src/domain/user/UseCases/LogoutUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { streamify, asyncInlineError } from '../../../decorators' 2 | import { UseCase } from '../../common/UseCase' 3 | 4 | @streamify('execute') 5 | class LogoutUserUseCase extends UseCase { 6 | #repository 7 | 8 | constructor({ repository }) { 9 | super() 10 | 11 | this.#repository = repository 12 | } 13 | 14 | @asyncInlineError() 15 | async execute() { 16 | const logoutUserStatus = await this.#repository.logout() 17 | 18 | return logoutUserStatus.toJSON() 19 | } 20 | } 21 | 22 | export { LogoutUserUseCase } 23 | -------------------------------------------------------------------------------- /src/domain/user/UseCases/RegisterUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { streamify, asyncInlineError } from '../../../decorators' 2 | import { UseCase } from '../../common/UseCase' 3 | 4 | @streamify('execute') 5 | class RegisterUserUseCase extends UseCase { 6 | #repository 7 | #usernameValueObjectFactory 8 | #passwordValueObjectFactory 9 | 10 | constructor({ 11 | repository, 12 | usernameValueObjectFactory, 13 | passwordValueObjectFactory, 14 | }) { 15 | super() 16 | this.#repository = repository 17 | this.#usernameValueObjectFactory = usernameValueObjectFactory 18 | this.#passwordValueObjectFactory = passwordValueObjectFactory 19 | } 20 | 21 | @asyncInlineError() 22 | async execute({ username, password }) { 23 | const user = await this.#repository.register({ 24 | username: this.#usernameValueObjectFactory({ username }), 25 | password: this.#passwordValueObjectFactory({ password }), 26 | }) 27 | 28 | return user.toJSON() 29 | } 30 | } 31 | 32 | export { RegisterUserUseCase } 33 | -------------------------------------------------------------------------------- /src/domain/user/UseCases/factories.ts: -------------------------------------------------------------------------------- 1 | import { UserServicesFactory } from '../Services/factory' 2 | import { UserRepositoriesFactory } from '../Repositories/factories' 3 | import { UserValueObjectsFactory } from '../ValueObjects/factories' 4 | 5 | import { CurrentUserUseCase } from './CurrentUserUseCase' 6 | import { RegisterUserUseCase } from './RegisterUserUseCase' 7 | import { LoginUserUseCase } from './LoginUserUseCase' 8 | import { LogoutUserUseCase } from './LogoutUserUseCase' 9 | 10 | const isNODE = typeof window === 'undefined' 11 | 12 | export class UserUseCasesFactory { 13 | static currentUserUseCase() { 14 | return new CurrentUserUseCase({ 15 | service: UserServicesFactory.currentUserService(), 16 | }) 17 | } 18 | 19 | static logoutUserUseCase() { 20 | return new LogoutUserUseCase({ 21 | repository: isNODE 22 | ? UserRepositoriesFactory.inMemoryUserRepository() 23 | : UserRepositoriesFactory.localStorageUserRepository(), 24 | }) 25 | } 26 | 27 | static loginUserUseCase() { 28 | return new LoginUserUseCase({ 29 | repository: isNODE 30 | ? UserRepositoriesFactory.inMemoryUserRepository() 31 | : UserRepositoriesFactory.localStorageUserRepository(), 32 | usernameValueObjectFactory: UserValueObjectsFactory.usernameValueObject, 33 | passwordValueObjectFactory: UserValueObjectsFactory.passwordValueObject, 34 | }) 35 | } 36 | 37 | static registerUserUseCase() { 38 | return new RegisterUserUseCase({ 39 | repository: isNODE 40 | ? UserRepositoriesFactory.inMemoryUserRepository() 41 | : UserRepositoriesFactory.localStorageUserRepository(), 42 | usernameValueObjectFactory: UserValueObjectsFactory.usernameValueObject, 43 | passwordValueObjectFactory: UserValueObjectsFactory.passwordValueObject, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/domain/user/ValueObjects/PasswordValueObject.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '../../common/ValueObject' 2 | 3 | const MIN_LENGTH = 8 4 | 5 | export class PasswordValueObject extends ValueObject { 6 | #password 7 | 8 | static validate({ password }) { 9 | if (!password || !password.length || password.length < MIN_LENGTH) { 10 | throw new Error( 11 | `[PasswordValueObject.validate] forbidden password lower than ${MIN_LENGTH} characters` 12 | ) 13 | } 14 | } 15 | 16 | constructor({ password }) { 17 | super() 18 | this.#password = password 19 | } 20 | 21 | value() { 22 | return this.#password 23 | } 24 | 25 | toJSON() { 26 | return { 27 | password: this.#password, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/domain/user/ValueObjects/StatusValueObject.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '../../common/ValueObject' 2 | 3 | export class StatusValueObject extends ValueObject { 4 | #status 5 | 6 | static validate({ status }) { 7 | if (typeof status !== 'boolean') { 8 | throw new Error( 9 | `[StatusValueObject.validate] status(${status}) should be boolean` 10 | ) 11 | } 12 | } 13 | 14 | constructor({ status }) { 15 | super() 16 | this.#status = status 17 | } 18 | 19 | isOK() { 20 | return this.#status 21 | } 22 | 23 | value() { 24 | return this.#status 25 | } 26 | 27 | toJSON() { 28 | return { 29 | status: this.#status, 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/domain/user/ValueObjects/UserNameValueObject.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '../../common/ValueObject' 2 | 3 | const MIN_LENGTH = 4 4 | 5 | export class UserNameValueObject extends ValueObject { 6 | #username 7 | 8 | static validate({ username }) { 9 | if (!username || !username.length || username.length < MIN_LENGTH) { 10 | throw new Error( 11 | `[UserNameValueObject.validate] forbidden username lower than ${MIN_LENGTH} characters` 12 | ) 13 | } 14 | } 15 | 16 | constructor({ username }) { 17 | super() 18 | 19 | this.#username = username 20 | } 21 | 22 | value() { 23 | return this.#username 24 | } 25 | 26 | toJSON() { 27 | return { 28 | username: this.#username, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/domain/user/ValueObjects/factories.ts: -------------------------------------------------------------------------------- 1 | import { PasswordValueObject } from './PasswordValueObject' 2 | import { UserNameValueObject } from './UserNameValueObject' 3 | import { StatusValueObject } from './StatusValueObject' 4 | 5 | export class UserValueObjectsFactory { 6 | static statusValueObject({ status }) { 7 | StatusValueObject.validate({ status }) 8 | 9 | return new StatusValueObject({ status }) 10 | } 11 | 12 | static passwordValueObject({ password }) { 13 | PasswordValueObject.validate({ password }) 14 | 15 | return new PasswordValueObject({ password }) 16 | } 17 | 18 | static usernameValueObject({ username }) { 19 | UserNameValueObject.validate({ username }) 20 | 21 | return new UserNameValueObject({ username }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-font-smoothing: antialiased; 3 | -moz-osx-font-smoothing: grayscale; 4 | } 5 | 6 | code { 7 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 8 | monospace; 9 | } 10 | 11 | html, body, #root { 12 | height:100%; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import CssBaseline from '@material-ui/core/CssBaseline' 4 | import { ThemeProvider } from '@material-ui/core/styles' 5 | import { Routes } from './Routes' 6 | import theme from './theme' 7 | import './index.css' 8 | 9 | import { Global } from './contexts/global' 10 | import { Pajarito } from './domain' 11 | 12 | const domain = new Pajarito() 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ) 23 | -------------------------------------------------------------------------------- /src/pages/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Container from '@material-ui/core/Container' 3 | import s from './Login.module.scss' 4 | import { LoginForm } from '../../components/LoginForm/LoginForm' 5 | 6 | export function Login() { 7 | return ( 8 | 9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/Login/Login.module.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | height: 100%; 3 | 4 | &__content { 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/Register/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Container from '@material-ui/core/Container' 3 | import s from './Register.module.scss' 4 | import { RegisterForm } from '../../components/RegisterForm/RegisterForm' 5 | 6 | export function Register() { 7 | return ( 8 | 9 |
10 | 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/Register/Register.module.scss: -------------------------------------------------------------------------------- 1 | .register { 2 | height: 100%; 3 | 4 | &__content { 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/Trinos/Trinos.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react' 2 | import { Layout } from '../../components/Layout/Layout' 3 | import Container from '@material-ui/core/Container' 4 | import s from './Trinos.module.scss' 5 | import { TrinoList } from '../../components/TrinoList/TrinoList' 6 | import PropTypes from 'prop-types' 7 | import { AddTrinoFavButton } from '../../components/AddTrinoFavButton/AddTrinoFavButton' 8 | 9 | import { Global } from '../../contexts/global' 10 | 11 | export function Trinos({ user }) { 12 | const { domain } = useContext(Global) 13 | const [trinos, setTrinos] = useState([]) 14 | const [error, setError] = useState(null) 15 | 16 | useEffect(() => { 17 | async function fn() { 18 | const [error, data] = await domain.get('listTrinoUseCase').execute() 19 | 20 | if (error) { 21 | return setError({ 22 | name: error.constructor.name, 23 | message: error.message, 24 | }) 25 | } 26 | 27 | return setTrinos(data.trinos) 28 | } 29 | 30 | fn() 31 | }, [domain]) 32 | 33 | useEffect(() => { 34 | const createTrinoUseCase$ = domain 35 | .get('createTrinoUseCase') 36 | .$.execute.subscribe( 37 | // eslint-disable-line 38 | ({ result }) => { 39 | setTrinos([result, ...trinos]) 40 | }, // eslint-disable-line 41 | (error) => { 42 | window.alert(error) 43 | } // eslint-disable-line 44 | ) 45 | 46 | return () => createTrinoUseCase$.dispose() 47 | }, [domain, trinos]) 48 | 49 | return ( 50 | 51 | 52 | {error ?
Error to get trinos
: } 53 |
54 | 55 | 56 |
57 | ) 58 | } 59 | 60 | Trinos.propTypes = { 61 | user: PropTypes.object, 62 | } 63 | -------------------------------------------------------------------------------- /src/pages/Trinos/Trinos.module.scss: -------------------------------------------------------------------------------- 1 | .trinos { 2 | &__container { 3 | padding-top: 1.5rem; 4 | padding-bottom: 1.5rem; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 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/extend-expect'; 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/tests/domain/user/CurrentUserUseCase.test.js: -------------------------------------------------------------------------------- 1 | import {Pajarito} from '../../domain' 2 | 3 | let domain 4 | describe('CurrentUserUseCase#execute', () => { 5 | beforeEach(() => { 6 | domain = new Pajarito() 7 | }) 8 | 9 | afterEach(() => { 10 | domain = null 11 | }) 12 | it('Happy path', async () => { 13 | const logout = await domain.get('logoutUserUseCase').execute() 14 | 15 | expect(logout.status).toEqual(true) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles' 2 | 3 | // A custom theme for this app 4 | const theme = createMuiTheme({ 5 | palette: { 6 | type: 'dark', 7 | primary: { 8 | main: '#0c7bc0', 9 | }, 10 | error: { 11 | main: '#c51f5d', 12 | }, 13 | background: { 14 | default: '#15212b', 15 | paper: '#182530', 16 | }, 17 | }, 18 | MuiAppBar: { 19 | colorPrimary: 'red', 20 | }, 21 | }) 22 | 23 | export default theme 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "experimentalDecorators": true 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------