├── .github └── workflows │ ├── deploy.yml │ └── pr.yml ├── .gitignore ├── client ├── .env ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .prettierrc ├── README.md ├── next.config.js ├── package.json ├── pages │ ├── _app.js │ ├── api │ │ └── hello.js │ ├── cars │ │ └── index.js │ ├── customers │ │ ├── add.js │ │ └── index.js │ ├── dashboard │ │ └── index.js │ ├── index.js │ └── rentals │ │ └── index.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── src │ ├── component │ │ ├── common │ │ │ └── infinite-scroll.js │ │ ├── customer │ │ │ └── customer-form.js │ │ ├── sidebar │ │ │ └── sidebar.js │ │ └── table │ │ │ └── load-more.js │ ├── redux │ │ └── reducer │ │ │ └── auth.js │ └── service │ │ ├── api │ │ ├── api.service.js │ │ ├── auth │ │ │ └── auth.service.js │ │ └── customer │ │ │ └── customer.service.js │ │ └── storage │ │ └── storage.service.js ├── styles │ ├── Home.module.css │ └── globals.css └── yarn.lock └── server ├── .dockerignore ├── .env ├── .env.test ├── .gitignore ├── .php-cs-fixer.dist.php ├── Dockerfile ├── Makefile ├── README.md ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── orm │ └── mapping │ │ ├── employee │ │ └── Employee.orm.xml │ │ └── rental │ │ ├── Car.orm.xml │ │ └── Rental.orm.xml ├── packages │ ├── cache.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── hautelook_alice.yaml │ ├── lexik_jwt_authentication.yaml │ ├── monolog.yaml │ ├── nelmio_alice.yaml │ ├── nelmio_api_doc.yaml │ ├── nelmio_cors.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── test │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── doctrine.yaml │ │ └── doctrine_migrations.yaml │ └── twig.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── framework.yaml │ └── nelmio_api_doc.yaml ├── services.yaml └── services │ ├── employee.yaml │ └── rental.yaml ├── docker-compose.yml.dist ├── docker ├── database │ ├── Dockerfile │ └── databases.sql └── php-apache │ ├── Dockerfile │ ├── default.conf │ └── xdebug.ini ├── fixtures └── Customer.yaml ├── migrations ├── dev │ └── Version20220518191517.php └── test │ └── Version20220518191518.php ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Customer │ ├── Adapter │ │ ├── Database │ │ │ └── ORM │ │ │ │ └── Doctrine │ │ │ │ ├── Mapping │ │ │ │ └── Customer.orm.xml │ │ │ │ └── Repository │ │ │ │ └── DoctrineCustomerRepository.php │ │ └── Framework │ │ │ ├── Config │ │ │ └── Services │ │ │ │ └── customer.yaml │ │ │ ├── Http │ │ │ ├── API │ │ │ │ ├── Filter │ │ │ │ │ └── CustomerFilter.php │ │ │ │ └── Response │ │ │ │ │ └── PaginatedResponse.php │ │ │ ├── ArgumentResolver │ │ │ │ └── RequestArgumentResolver.php │ │ │ ├── Controller │ │ │ │ ├── Customer │ │ │ │ │ ├── CreateCustomerController.php │ │ │ │ │ ├── DeleteCustomerController.php │ │ │ │ │ ├── GetCustomerByIdController.php │ │ │ │ │ ├── SearchCustomerController.php │ │ │ │ │ └── UpdateCustomerController.php │ │ │ │ └── HealthCheckController.php │ │ │ ├── DTO │ │ │ │ ├── CreateCustomerRequestDTO.php │ │ │ │ ├── DeleteCustomerRequestDTO.php │ │ │ │ ├── GetCustomerByIdRequestDTO.php │ │ │ │ ├── GetCustomersRequest.php │ │ │ │ ├── RequestDTO.php │ │ │ │ └── UpdateCustomerRequestDTO.php │ │ │ └── RequestTransformer │ │ │ │ └── RequestTransformer.php │ │ │ └── Listener │ │ │ └── JsonTransformerExceptionListener.php │ ├── Application │ │ └── UseCase │ │ │ └── Customer │ │ │ ├── CreateCustomer │ │ │ ├── CreateCustomer.php │ │ │ └── DTO │ │ │ │ ├── CreateCustomerInputDTO.php │ │ │ │ └── CreateCustomerOutputDTO.php │ │ │ ├── DeleteCustomer │ │ │ ├── DTO │ │ │ │ └── DeleteCustomerInputDTO.php │ │ │ └── DeleteCustomer.php │ │ │ ├── GetCustomerById │ │ │ ├── DTO │ │ │ │ ├── GetCustomerByIdInputDTO.php │ │ │ │ └── GetCustomerByIdOutputDTO.php │ │ │ └── GetCustomerById.php │ │ │ ├── Search │ │ │ ├── DTO │ │ │ │ └── SearchCustomersOutput.php │ │ │ └── SearchCustomers.php │ │ │ └── UpdateCustomer │ │ │ ├── DTO │ │ │ ├── UpdateCustomerInputDTO.php │ │ │ └── UpdateCustomerOutputDTO.php │ │ │ └── UpdateCustomer.php │ ├── Domain │ │ ├── Exception │ │ │ ├── CustomerAlreadyExistsException.php │ │ │ ├── InvalidArgumentException.php │ │ │ └── ResourceNotFoundException.php │ │ ├── Model │ │ │ └── Customer.php │ │ ├── Repository │ │ │ └── CustomerRepository.php │ │ ├── Validation │ │ │ └── Traits │ │ │ │ ├── AssertLengthRangeTrait.php │ │ │ │ ├── AssertMinimumAgeTrait.php │ │ │ │ └── AssertNotNullTrait.php │ │ └── ValueObject │ │ │ └── Uuid.php │ └── Service │ │ └── .gitignore ├── Employee │ ├── Command │ │ ├── CreateEmployeeCommand.php │ │ └── RemoveEmployeeCommand.php │ ├── Controller │ │ ├── CreateCustomerController.php │ │ ├── DeleteCustomerController.php │ │ ├── GetCustomersController.php │ │ └── HealthCheckController.php │ ├── Entity │ │ └── Employee.php │ ├── Exception │ │ ├── DatabaseException.php │ │ ├── EmployeeAlreadyExistsException.php │ │ └── ResourceNotFoundException.php │ ├── Http │ │ ├── HttpClient.php │ │ └── HttpClientInterface.php │ ├── Repository │ │ ├── DoctrineEmployeeRepository.php │ │ └── EmployeeRepository.php │ └── Service │ │ ├── CreateCustomerService.php │ │ ├── CreateEmployeeService.php │ │ ├── DeleteCustomerService.php │ │ ├── GetEmployeeCustomers.php │ │ ├── RemoveEmployeeService.php │ │ └── Security │ │ ├── Listener │ │ └── JWTCreatedListener.php │ │ ├── PasswordHasherInterface.php │ │ ├── SymfonyPasswordHasher.php │ │ └── Voter │ │ └── EmployeeVoter.php ├── Kernel.php └── Rental │ ├── Controller │ └── HealthCheckController.php │ ├── Entity │ ├── Car.php │ └── Rental.php │ ├── Repository │ ├── DoctrineRentRepository.php │ └── RentRepository.php │ └── Service │ └── .gitignore ├── symfony.lock ├── templates └── base.html.twig └── tests ├── Functional └── Customer │ └── Controller │ ├── Customer │ ├── CreateCustomerControllerTest.php │ ├── DeleteCustomerControllerTest.php │ └── UpdateCustomerControllerTest.php │ ├── CustomerControllerTestBase.php │ └── HealthCheckControllerTest.php ├── Unit ├── Customer │ └── Application │ │ └── UseCase │ │ └── Customer │ │ ├── CreateCustomer │ │ ├── CreateCustomerTest.php │ │ └── DTO │ │ │ └── CreateCustomerInputDTOTest.php │ │ ├── DeleteCustomer │ │ └── DeleteCustomerTest.php │ │ ├── GetCustomerById │ │ ├── DTO │ │ │ └── GetCustomerByIdInputDTOTest.php │ │ └── GetCustomerByIdTest.php │ │ └── UpdateCustomer │ │ ├── DTO │ │ └── UpdateCustomerInputDTOTest.php │ │ └── UpdateCustomerTest.php └── Employee │ └── Service │ ├── CreateEmployeeServiceTest.php │ └── RemoveEmployeeServiceTest.php └── bootstrap.php /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | name: Deploy on production 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Deploy commands 13 | uses: appleboy/ssh-action@master 14 | with: 15 | host: ${{ secrets.HOST }} 16 | username: ${{ secrets.USERNAME }} 17 | key: ${{ secrets.KEY }} 18 | port: ${{ secrets.PORT }} 19 | script: | 20 | ./deploy.sh 21 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - main 5 | 6 | jobs: 7 | app_checks: 8 | name: app checks 9 | runs-on: ubuntu-latest 10 | defaults: 11 | run: 12 | working-directory: server 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Pull docker-compose 17 | run: docker-compose -f docker-compose.yml.dist pull 18 | - uses: satackey/action-docker-layer-caching@v0.0.11 19 | continue-on-error: true 20 | - name: Prepare containers 21 | run: make start && make composer-install 22 | - name: Run coding style check 23 | run: make code-style-check 24 | - name: buffering time for db container 25 | uses: jakejarvis/wait-action@master 26 | with: 27 | time: '10s' 28 | - name: Run tests 29 | run: make migrations-test && make generate-ssh-keys && make tests 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_BASE_URL=http://localhost:1000 2 | NEXT_PUBLIC_API_PATH=/api 3 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /client/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /client/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "lint:fix": "next lint --fix", 11 | "format": "prettier --check --ignore-path .gitignore .", 12 | "format:fix": "prettier --write --ignore-path .gitignore ." 13 | }, 14 | "dependencies": { 15 | "@chakra-ui/icons": "^2.0.12", 16 | "@chakra-ui/react": "^2.3.1", 17 | "@emotion/react": "^11.10.4", 18 | "@emotion/styled": "^11.10.4", 19 | "@hookform/resolvers": "^2.9.6", 20 | "axios": "^1.1.2", 21 | "framer-motion": "^7.2.1", 22 | "jwt-decode": "^3.1.2", 23 | "lodash": "^4.17.21", 24 | "next": "12.2.3", 25 | "next-redux-wrapper": "^8.0.0", 26 | "react": "18.2.0", 27 | "react-bootstrap": "^2.4.0", 28 | "react-dom": "18.2.0", 29 | "react-hook-form": "^7.33.1", 30 | "react-icons": "^4.4.0", 31 | "react-redux": "^8.0.4", 32 | "redux": "^4.2.0", 33 | "yup": "^0.32.11" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^8.20.0", 37 | "eslint-config-next": "^12.2.3", 38 | "eslint-config-prettier": "^8.5.0", 39 | "prettier": "^2.7.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from '@chakra-ui/react' 2 | import axios from 'axios' 3 | import { combineReducers, legacy_createStore as createStore } from 'redux' 4 | import AuthReducer, { 5 | fromLocalStorage, 6 | logout, 7 | } from '../src/redux/reducer/auth' 8 | import { loadState, saveState } from '../src/service/storage/storage.service' 9 | 10 | import throttle from 'lodash/throttle' 11 | import { createWrapper } from 'next-redux-wrapper' 12 | import { Provider, useSelector } from 'react-redux' 13 | 14 | const rootReducer = combineReducers({ 15 | auth: AuthReducer, 16 | }) 17 | 18 | const store = createStore(rootReducer) 19 | const makeStore = () => store 20 | const wrapper = createWrapper(makeStore) 21 | 22 | loadState() 23 | .then((state) => { 24 | undefined !== state && store.dispatch(fromLocalStorage(state.auth)) 25 | }) 26 | .catch((err) => console.log(err)) 27 | 28 | store.subscribe( 29 | throttle(() => { 30 | saveState({ 31 | auth: store.getState().auth, 32 | }) 33 | }, 1000), 34 | ) 35 | 36 | function MyApp({ Component, pageProps }) { 37 | const token = useSelector((state) => state.auth.token) 38 | 39 | axios.defaults.baseURL = process.env.NEXT_PUBLIC_API_BASE_URL 40 | axios.defaults.headers.Authorization = `Bearer ${token}` 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default wrapper.withRedux(MyApp) 52 | -------------------------------------------------------------------------------- /client/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /client/pages/cars/index.js: -------------------------------------------------------------------------------- 1 | import { Heading } from '@chakra-ui/react' 2 | import SidebarWithHeader from '../../src/component/sidebar/sidebar' 3 | 4 | export default function Cars() { 5 | return ( 6 | 7 | Cars list! 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /client/pages/customers/add.js: -------------------------------------------------------------------------------- 1 | import { Box, Heading, useToast } from '@chakra-ui/react' 2 | import { useRouter } from 'next/router' 3 | 4 | import * as yup from 'yup' 5 | import { yupResolver } from '@hookform/resolvers/yup' 6 | import { useForm } from 'react-hook-form' 7 | import { useSelector } from 'react-redux' 8 | import SidebarWithHeader from '../../src/component/sidebar/sidebar' 9 | import CustomerForm from '../../src/component/customer/customer-form' 10 | import { createCustomer } from '../../src/service/api/customer/customer.service' 11 | 12 | export default function Add() { 13 | const router = useRouter() 14 | const toast = useToast() 15 | const employeeId = useSelector((state) => state.auth.id) 16 | 17 | const validationSchema = yup.object().shape({ 18 | name: yup 19 | .string() 20 | .required('Debes introducir el nombre del cliente para poder guardarlo'), 21 | email: yup.string().required('Email obligatorio').email('Email no válido'), 22 | age: yup 23 | .number() 24 | .typeError() 25 | .required('Edad obligatoria') 26 | .min(18, 'La edad mínima debe ser 18 años o superior') 27 | .max(150, 'La edad no puede ser superior a 150 años'), 28 | address: yup.string().required('Debes introducir una dirección'), 29 | }) 30 | 31 | const { 32 | register, 33 | handleSubmit, 34 | formState: { errors }, 35 | } = useForm({ 36 | resolver: yupResolver(validationSchema), 37 | }) 38 | 39 | const onSubmitForm = async (data) => { 40 | try { 41 | await createCustomer(employeeId, { 42 | name: data.name, 43 | email: data.email, 44 | age: data.age, 45 | address: data.address, 46 | }) 47 | await router.push('/customers') 48 | } catch (e) { 49 | if (409 === e.response.status) { 50 | toast({ 51 | title: 'No se ha podido crear al cliente.', 52 | description: `Ya existe un cliente en la base de datos con email ${data.email}`, 53 | status: 'error', 54 | duration: 5000, 55 | isClosable: true, 56 | }) 57 | } 58 | } 59 | } 60 | 61 | return ( 62 | 63 | Nuevo cliente 64 | 65 | router.back()} 70 | /> 71 | 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /client/pages/customers/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Flex, 4 | FormControl, 5 | Heading, 6 | Icon, 7 | IconButton, 8 | Input, 9 | Spacer, 10 | } from '@chakra-ui/react' 11 | import SidebarWithHeader from '../../src/component/sidebar/sidebar' 12 | import { useSelector } from 'react-redux' 13 | import { 14 | deleteCustomer, 15 | searchCustomers, 16 | } from '../../src/service/api/customer/customer.service' 17 | import { useCallback, useEffect, useState } from 'react' 18 | import { 19 | Table, 20 | Thead, 21 | Tbody, 22 | Tr, 23 | Th, 24 | Td, 25 | TableContainer, 26 | } from '@chakra-ui/react' 27 | import InfiniteScroll from '../../src/component/common/infinite-scroll' 28 | import { FiChevronDown, FiChevronUp, FiPlus, FiTrash } from 'react-icons/fi' 29 | import { useRouter } from 'next/router' 30 | 31 | export default function Customers() { 32 | const id = useSelector((state) => state.auth.id) 33 | const router = useRouter() 34 | const [customers, setCustomers] = useState([]) 35 | const [meta, setMeta] = useState({ 36 | page: 1, 37 | limit: 30, 38 | hasNext: false, 39 | total: 0, 40 | }) 41 | const [filters, setFilters] = useState({ 42 | name: '', 43 | }) 44 | const [sorting, setSorting] = useState({ 45 | sort: 'name', 46 | order: 'asc', 47 | }) 48 | 49 | const buildFilters = useCallback( 50 | (page, limit) => { 51 | let result = `?page=${page}&limit=${limit}&sort=${sorting.sort}&order=${sorting.order}` 52 | 53 | if ('' !== filters.name) { 54 | result += `&name=${filters.name}` 55 | } 56 | 57 | return result 58 | }, 59 | [filters, sorting], 60 | ) 61 | 62 | const search = useCallback( 63 | async (page = 1, limit = 30, loadMore = false) => { 64 | try { 65 | const response = await searchCustomers(id, buildFilters(page, limit)) 66 | setCustomers( 67 | loadMore 68 | ? customers.concat(response.data.items) 69 | : response.data.items, 70 | ) 71 | setMeta(response.data.meta) 72 | } catch (e) { 73 | console.log(e) 74 | } 75 | }, 76 | [id, customers, buildFilters], 77 | ) 78 | 79 | useEffect(() => { 80 | search() 81 | }, [filters, sorting]) // eslint-disable-line 82 | 83 | const remove = async (customerId) => { 84 | try { 85 | await deleteCustomer(id, customerId) 86 | setCustomers(customers.filter((customer) => customerId !== customer.id)) 87 | setMeta((prevState) => { 88 | return { 89 | ...prevState, 90 | total: meta.total - 1, 91 | } 92 | }) 93 | } catch (e) { 94 | console.log(e) 95 | } 96 | } 97 | 98 | return ( 99 | 100 | Customers list 101 | 102 | 103 | 104 | { 110 | if ('' === e.target.value) { 111 | setFilters((prevState) => { 112 | return { 113 | ...prevState, 114 | name: e.target.value, 115 | } 116 | }) 117 | } 118 | }} 119 | onKeyDown={(e) => { 120 | if (e.code === 'Enter') { 121 | setFilters((prevState) => { 122 | return { 123 | ...prevState, 124 | name: e.target.value, 125 | } 126 | }) 127 | } 128 | }} 129 | /> 130 | 131 | 132 | 133 | 134 | } 139 | onClick={() => router.push('/customers/add')} 140 | /> 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 166 | 167 | 168 | 169 | 170 | 171 | {customers.map((customer) => ( 172 | 173 | 174 | 175 | 176 | 183 | 184 | ))} 185 | 186 |
ID 149 | Name 150 | { 156 | setSorting((prevState) => { 157 | return { 158 | ...prevState, 159 | sort: 'name', 160 | order: 'asc' === prevState.order ? 'desc' : 'asc', 161 | } 162 | }) 163 | }} 164 | /> 165 | AddressActions
{customer.id}{customer.name}{customer.address} 177 | remove(customer.id)} 181 | /> 182 |
187 |
188 | 189 |
190 | ) 191 | } 192 | -------------------------------------------------------------------------------- /client/pages/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import { Heading } from '@chakra-ui/react' 2 | import SidebarWithHeader from '../../src/component/sidebar/sidebar' 3 | 4 | export default function Dashboard() { 5 | return ( 6 | 7 | Welcome to Codenip Car Rental System! 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /client/pages/index.js: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup' 2 | import { yupResolver } from '@hookform/resolvers/yup' 3 | import { useForm, Controller } from 'react-hook-form' 4 | import { 5 | Flex, 6 | Heading, 7 | Input, 8 | Button, 9 | InputGroup, 10 | Stack, 11 | Box, 12 | Avatar, 13 | FormControl, 14 | InputRightElement, 15 | Text, 16 | useToast, 17 | useColorMode, 18 | } from '@chakra-ui/react' 19 | import { useEffect, useState } from 'react' 20 | import { decodeToken, login } from '../src/service/api/auth/auth.service' 21 | import { useRouter } from 'next/router' 22 | import { useDispatch, useSelector } from 'react-redux' 23 | import { saveUser } from '../src/redux/reducer/auth' 24 | 25 | export default function Home() { 26 | const { colorMode, toggleColorMode } = useColorMode() 27 | const router = useRouter() 28 | const dispatch = useDispatch() 29 | const token = useSelector((state) => state.auth.token) 30 | const toast = useToast() 31 | 32 | const validationSchema = yup.object().shape({ 33 | email: yup.string().email('Invalid email').required('Email is mandatory'), 34 | password: yup 35 | .string() 36 | .required('Password is mandatory') 37 | .min(6, 'Password must be at least 6 characters'), 38 | }) 39 | 40 | const { 41 | control, 42 | handleSubmit, 43 | formState: { errors }, 44 | } = useForm({ 45 | resolver: yupResolver(validationSchema), 46 | }) 47 | 48 | const [showPassword, setShowPassword] = useState(false) 49 | 50 | const handleShowClick = () => setShowPassword(!showPassword) 51 | 52 | const onSubmitForm = async (data) => { 53 | try { 54 | const response = await login(data.email.trim(), data.password.trim()) 55 | const token = response.data.token 56 | const payload = decodeToken(token) 57 | 58 | dispatch(saveUser(token, payload)) 59 | 60 | await router.push('/dashboard') 61 | } catch (e) { 62 | toast({ 63 | title: 'Invalid credential.', 64 | description: 'Invalid email or password. Please try again!', 65 | status: 'error', 66 | duration: 5000, 67 | isClosable: true, 68 | }) 69 | } 70 | } 71 | 72 | useEffect(() => { 73 | async function toDashboard() { 74 | await router.push('/dashboard') 75 | } 76 | 77 | if (undefined !== token) { 78 | toDashboard() 79 | } 80 | }) 81 | 82 | useEffect(() => { 83 | if ('dark' === colorMode) { 84 | toggleColorMode() 85 | } 86 | }) 87 | 88 | return ( 89 | 97 | 103 | 104 | Codenip Car Rental 105 | 106 |
107 | 113 | 114 | 115 | ( 120 | 127 | )} 128 | /> 129 | 130 | 131 | {errors.email?.message} 132 | 133 | 134 | 135 | 136 | ( 141 | 148 | )} 149 | /> 150 | 151 | 154 | 155 | 156 | 157 | {errors.password?.message} 158 | 159 | 160 | 169 | 170 |
171 |
172 |
173 |
174 | ) 175 | } 176 | -------------------------------------------------------------------------------- /client/pages/rentals/index.js: -------------------------------------------------------------------------------- 1 | import { Heading } from '@chakra-ui/react' 2 | import SidebarWithHeader from '../../src/component/sidebar/sidebar' 3 | 4 | export default function Rentals() { 5 | return ( 6 | 7 | Rentals list! 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codenip-tech/modular-monolith-example/62c7d29bbaa125de733d9f91a7c65a238de5e280/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/component/common/infinite-scroll.js: -------------------------------------------------------------------------------- 1 | import { Center, Text } from '@chakra-ui/react' 2 | import LoadMore from '../table/load-more' 3 | 4 | export default function InfiniteScroll({ meta, collection, search }) { 5 | return meta.hasNext ? ( 6 | <> 7 |
8 | search(meta.page + 1, meta.limit, true)} /> 9 |
10 | 11 | Showing {collection.length} of {meta.total} results 12 | 13 | 14 | ) : ( 15 | 16 | Showing {collection.length} of {meta.total} results 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /client/src/component/customer/customer-form.js: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Flex, 4 | FormControl, 5 | Input, 6 | InputGroup, 7 | NumberDecrementStepper, 8 | NumberIncrementStepper, 9 | NumberInput, 10 | NumberInputField, 11 | NumberInputStepper, 12 | Stack, 13 | Text, 14 | Textarea, 15 | } from '@chakra-ui/react' 16 | 17 | export default function CustomerForm({ onSubmit, register, errors, goBack }) { 18 | return ( 19 |
20 | 21 | 22 | 23 | 29 | 30 | 31 | {errors.name?.message} 32 | 33 | 34 | 35 | 36 | 42 | 43 | 44 | {errors.email?.message} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {errors.age?.message} 59 | 60 | 61 | 62 | 63 |