├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public
├── _redirects
└── index.html
├── src
├── App.tsx
├── assets
│ └── images
│ │ ├── arrow-right.svg
│ │ ├── clientes.svg
│ │ ├── entregas.svg
│ │ ├── home.svg
│ │ └── icon-plus.svg
├── components
│ ├── Content
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Header
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Icon
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── MainContainer
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Modal
│ │ ├── DeleteCustomerModal
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── NewOccurrenceModal
│ │ │ └── index.tsx
│ │ ├── SearchCustomerModal
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── NavLink
│ │ └── index.tsx
│ └── SideBar
│ │ ├── index.tsx
│ │ └── styles.ts
├── hooks
│ ├── useCustomers.tsx
│ └── useDeliveries.tsx
├── index.tsx
├── mirage
│ └── index.ts
├── react-app-env.d.ts
├── routes.tsx
├── services
│ └── api.ts
├── shared
│ ├── Button
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Input
│ │ └── index.tsx
│ └── Textarea
│ │ └── index.tsx
├── styles
│ ├── global.ts
│ └── view
│ │ ├── create-new-delivery.ts
│ │ ├── customers.ts
│ │ ├── dashboard.ts
│ │ ├── deliveries.ts
│ │ ├── delivery-details.ts
│ │ └── registration-customer.ts
├── types.ts
├── utils
│ └── format.ts
└── view
│ ├── CreateNewDeliveryView.tsx
│ ├── CustomersView.tsx
│ ├── DashboardView.tsx
│ ├── DeliveriesView.tsx
│ ├── DeliveryDetailsView.tsx
│ └── RegistrationCustomerView.tsx
├── tsconfig.json
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Glauber de Oliveira Matos
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 🚧 Aplicação finalizada! 🚧
23 |
24 |
25 | ## 🏁 Tópicos
26 |
27 |
28 | 👉 Sobre
29 | 👉 Funcionalidades
30 | 👉 Melhorias
31 |
32 |
33 | 👉 Como executar
34 | 👉 Tecnologias
35 | 👉 Autor
36 | 👉 Licença
37 |
38 |
39 |
40 | ## 💻 Sobre o projeto
41 |
42 | O Algalog-web é uma aplicação que permite gerenciar os pedidos de entrega de uma empresa de logistica.
43 |
44 | O objetivo deste projeto era se desafiar a criar todo o frontend desde o UI Design até o resultado final em ReactJS. Para então, conectar com o backend java desenvolvido durante um treinamento gratuito sobre api-rest com spring-boot, saiba mais sobre a api **[AQUI](https://github.com/glaubermatos/algalog-api)**.
45 |
46 | Confira o resultado no link abaixo:
47 |
48 |
49 |
50 |
51 |
52 | ---
53 |
54 |
55 |
56 | ## ⚙️ Funcionalidades
57 |
58 | - [x] Página inicial com informações sobre a quantidade de entregas e clientes;
59 | - [x] Cadastrar novo clientes;
60 | - [x] Atualizar dados do cliente;
61 | - [x] Solicitar nova entrega;
62 | - [x] Pesquisar cliente por nome;
63 | - [x] Listagem das solicitações de entrega;
64 | - [x] Listagem dos clientes;
65 | - [x] Excluir usuário (desde que não tenha entregas cadastradas em seu nome);
66 | - [x] Visualizar dados da solicitação de entrega;
67 | - [x] MUdanças de status da entrega (Em andamento, Cancelada, Finalizada);
68 | - [x] Adicionar ocorrencias (situações que impeçam a entrega);
69 |
70 | ---
71 |
72 |
73 | ## ⚙️ Melhorias
74 | - [ ] Trabalhar o leyout responsivo;
75 | - [ ] Mostrar um load de carregamento nas páginas enquanto o servidor não retorna os dados do backend;
76 |
77 | ---
78 |
79 |
85 |
86 |
95 |
96 | ## 🚀 Como executar o projeto
97 |
98 | ### Pré-requisitos
99 |
100 | Antes de começar, você vai precisar ter instalado em sua máquina as seguintes ferramentas:
101 | [Git](https://git-scm.com), [Node.js](https://nodejs.org/en/), [Yarn](https://classic.yarnpkg.com/en/docs/install).
102 | Além disto é bom ter um editor para trabalhar com o código como [VSCode](https://code.visualstudio.com/)
103 |
104 |
105 | #### 🧭 Rodando a aplicação web (Frontend)
106 |
107 | ```bash
108 |
109 | # Clone este repositório
110 | $ git clone https://github.com/glaubermatos/algalog-web.git
111 |
112 | # Acesse a pasta do projeto no seu terminal/cmd
113 | $ cd algalog-web
114 |
115 | # Instale as dependências
116 | $ yarn
117 |
118 | # Execute a aplicação em modo de desenvolvimento
119 | $ yarn start
120 |
121 | # A aplicação será aberta na porta:3000 - acesse http://localhost:3000
122 |
123 | ```
124 |
125 | ---
126 |
127 | ## 🛠 Tecnologias
128 |
129 | Este é um projeto criado com o **[Create React App](https://github.com/facebook/create-react-app)**.
130 | As seguintes ferramentas foram usadas na construção do projeto:
131 |
132 | #### **Aplicação web** ([ReactJS](https://pt-br.reactjs.org/) + [TypeScript](https://www.typescriptlang.org/))
133 |
134 | - **[Styled Components](https://styled-components.com/)**
135 | - **[React Router DOM](https://reactrouter.com/)**
136 | - **[Axios](https://github.com/axios/axios)**
137 | - **[React Icons](https://react-icons.github.io/react-icons/)**
138 | - **[React Modal](http://reactcommunity.org/react-modal/)**
139 | - **[React-Toastify](https://fkhadra.github.io/react-toastify/introduction)**
140 | - **[MirageJS](https://miragejs.com/)**
141 |
142 |
143 | > Veja o arquivo [package.json](https://github.com/glaubermatos/algalog-web/blob/main/package.json)
144 |
145 | #### **Utilitários**
146 |
147 | - Editor: **[Visual Studio Code](https://code.visualstudio.com/)**
148 | - Ícones: **[Feather Icons](https://feathericons.com/)**
149 | - Fontes: **[Inter](https://fonts.google.com/specimen/Inter)** | **[Nunito](https://fonts.google.com/specimen/Nunito)**
150 |
151 | ---
152 |
153 |
154 |
155 | ## 🦸♂️ **Autor**
156 |
157 |
158 |
159 |
160 | 🌟 Glauber de Oliveira Matos 🌟
161 |
162 |
163 | [](https://www.linkedin.com/in/glaubermatos/)
164 |
165 | ---
166 |
167 |
168 | ## 📝 Licença
169 |
170 | Este projeto esta sobe a licença [MIT](./LICENSE).
171 |
172 | Feito com :satisfied: por Glauber de Oliveira Matos 👋🏽 [Entre em contato!](https://www.linkedin.com/in/glaubermatos/)
173 |
174 | ---
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "algalog-web",
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": "^17.0.0",
12 | "@types/react-dom": "^17.0.0",
13 | "axios": "^0.23.0",
14 | "react": "^17.0.2",
15 | "react-dom": "^17.0.2",
16 | "react-icons": "^4.3.1",
17 | "react-input-mask": "^3.0.0-alpha.2",
18 | "react-modal": "^3.14.3",
19 | "react-router-dom": "^5.3.0",
20 | "react-scripts": "4.0.3",
21 | "react-toastify": "^8.0.3",
22 | "styled-components": "^5.3.1",
23 | "typescript": "^4.1.2",
24 | "web-vitals": "^1.0.1"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "@types/react-input-mask": "^3.0.1",
52 | "@types/react-modal": "^3.13.1",
53 | "@types/react-router-dom": "^5.3.0",
54 | "@types/styled-components": "^5.1.14",
55 | "miragejs": "^0.1.42"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | AlgaLog
11 |
12 |
13 |
14 |
15 |
16 | You need to enable JavaScript to run this app.
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter} from 'react-router-dom'
2 | import { ToastContainer } from 'react-toastify';
3 |
4 | import { Routes } from './routes';
5 |
6 | import { DeliveriesProvider } from './hooks/useDeliveries';
7 | import { CustomersProvider } from './hooks/useCustomers';
8 |
9 | import { SideBar } from "./components/SideBar";
10 | import { MainContainer } from "./components/MainContainer";
11 | import { Content } from './components/Content';
12 |
13 | import { GlobalStyle } from "./styles/global";
14 | import 'react-toastify/dist/ReactToastify.min.css'
15 |
16 | export function App() {
17 |
18 | return (
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
--------------------------------------------------------------------------------
/src/assets/images/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/clientes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/entregas.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/images/home.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/icon-plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/components/Content/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Container } from "./styles";
3 |
4 | interface ContentProps {
5 | children: ReactNode
6 | }
7 |
8 | export function Content(props: ContentProps) {
9 | return(
10 |
11 | {props.children}
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/src/components/Content/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | max-width: 1120px;
5 | margin: 0 auto;
6 | `
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "./styles";
2 | import { Icon } from "../Icon";
3 | import { Title } from "../../styles/global";
4 | import { ReactNode } from "react";
5 |
6 | interface HeaderProps {
7 | iconName?: 'home' | 'entregas' | 'clientes';
8 | title: string;
9 | helpText?: string;
10 | children?: ReactNode;
11 | }
12 |
13 | export function Header({iconName, title, helpText, children}: HeaderProps) {
14 | return(
15 |
16 |
17 | {iconName ? (
18 |
19 | ) : (<>>)}
20 |
21 |
{ title }
22 | {helpText ? (
23 | { helpText }
24 | ) : (<>>)}
25 |
26 |
27 |
28 | {children}
29 |
30 |
31 | );
32 | }
--------------------------------------------------------------------------------
/src/components/Header/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.header`
4 | border-bottom: 1px solid var(--gray-200);
5 | display: flex;
6 | justify-content: space-between;
7 | align-items: flex-start;
8 |
9 | .info-container {
10 | border-bottom: 1px solid var(--gray-600);
11 | padding-bottom: 1rem;
12 |
13 | display: flex;
14 | gap: 1.5rem;
15 | }
16 |
17 | .box-title {
18 |
19 | span {
20 | display: block;
21 | margin-top: 0.5rem;
22 | font-weight: 400;
23 | color: var(--gray-400);
24 | }
25 | }
26 |
27 | .actions {
28 |
29 | }
30 | `
--------------------------------------------------------------------------------
/src/components/Icon/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconSvg } from './styles'
2 |
3 | interface IconProps {
4 | name: string;
5 | size?: number;
6 | color?: 'primary-light' | 'gray';
7 | strokeWidth?: string
8 | }
9 |
10 | export function Icon(props: IconProps) {
11 |
12 | const { name: iconName, size, color, strokeWidth = '2' } = props;
13 |
14 | return(
15 | renderIcon(iconName)
16 | );
17 |
18 | function renderIcon(iconName: string) {
19 | switch (iconName) {
20 | case 'home':
21 | return (
22 |
23 |
24 | )
25 |
26 | case 'entregas':
27 | return (
28 |
29 |
30 |
31 |
32 | )
33 |
34 | case 'clientes':
35 | return (
36 |
37 |
38 | )
39 |
40 | default:
41 | return <>>
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/components/Icon/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | interface IconSvgProps {
4 | color?: 'primary-light' | 'gray';
5 | }
6 |
7 | export const IconSvg = styled.svg`
8 |
9 | & path {
10 | stroke: ${(props) => {
11 | switch (props.color) {
12 | case 'gray':
13 | return 'var(--gray-600)';
14 |
15 | case 'primary-light':
16 | return 'var(--primary-color-light)';
17 |
18 | default:
19 | return 'var(--background-color)';
20 | }
21 | }};
22 |
23 |
24 | }
25 | `
--------------------------------------------------------------------------------
/src/components/MainContainer/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Container } from "./styles";
3 |
4 | interface MainContainerProps {
5 | children: ReactNode
6 | }
7 |
8 | export function MainContainer(props: MainContainerProps) {
9 | return(
10 |
11 | {props.children}
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/src/components/MainContainer/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.main`
4 | margin-left: var(--sidebar-width);
5 | padding-top: 4rem;
6 |
7 | `
--------------------------------------------------------------------------------
/src/components/Modal/DeleteCustomerModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { FiTrash2 } from "react-icons/fi";
2 |
3 | import { Modal } from "../";
4 | import { Button } from "../../../shared/Button";
5 |
6 | import { Customer } from "../../../types";
7 | import { Content } from "./styles";
8 |
9 | interface DeleteCustomerModalProps {
10 | isDeleteCustomerModalOpen: boolean;
11 | customerToDelete: Customer;
12 | onRequestClose: () => void;
13 | onDeleteCustomer: (customer: Customer) => void;
14 | }
15 |
16 | export function DeleteCustomerModal({
17 | customerToDelete,
18 | isDeleteCustomerModalOpen,
19 | onRequestClose,
20 | onDeleteCustomer }: DeleteCustomerModalProps) {
21 |
22 |
23 | function handleCloseDeleteCustomerModal() {
24 | onRequestClose()
25 | }
26 |
27 | function handleDeleteCustomer(customer: Customer) {
28 | onDeleteCustomer(customer)
29 | }
30 |
31 | return(
32 |
37 |
38 |
39 | {customerToDelete.nome}
40 | Quer mesmo excluir esse cliente? Ele será removido pra sempre.
41 |
42 |
46 | Cancelar
47 |
48 | handleDeleteCustomer(customerToDelete)}
51 | >
52 | Excluir cliente
53 |
54 |
55 |
56 |
57 | )
58 | }
--------------------------------------------------------------------------------
/src/components/Modal/DeleteCustomerModal/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../../../styles/global";
3 |
4 | export const Content = styled(PageContent)`
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 |
9 | svg {
10 | stroke: var(--danger-color);
11 | }
12 |
13 | span {
14 | text-transform: uppercase;
15 | margin-top: 2rem;
16 | margin-bottom: 1.5rem;
17 | font: 500 1rem 'Inter', sans-serif;
18 | color: var(--gray-800);
19 | }
20 |
21 | p {
22 | margin-bottom: 3rem;
23 | font: 500 1.125rem 'Inter', sans-serif;
24 | line-height: 1.40625rem;
25 | max-width: 19rem;
26 | text-align: center;
27 | color: var(--gray-400);
28 | }
29 |
30 | .modal-actions {
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | gap: 1rem;
35 |
36 | button.default {
37 | color: var(--danger-color);
38 | }
39 | }
40 | `
--------------------------------------------------------------------------------
/src/components/Modal/NewOccurrenceModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent, useState } from "react";
2 | import { toast } from "react-toastify";
3 |
4 | import { api } from "../../../services/api";
5 |
6 | import { Modal } from "../";
7 |
8 | import { Button } from "../../../shared/Button";
9 | import { Textarea } from "../../../shared/Textarea";
10 |
11 | interface NewOccurrenceProps {
12 | isOpen: boolean;
13 | onRequestClose: () => void;
14 | onLoadOccurrences: () => void;
15 | entregaId: number;
16 | }
17 |
18 | export function NewOccurrenceModal({isOpen, onRequestClose, onLoadOccurrences, entregaId}: NewOccurrenceProps) {
19 |
20 | const [descricao, setDescricao] = useState('')
21 |
22 | function handleSubmit(event: FormEvent) {
23 | event.preventDefault()
24 |
25 | const data = {
26 | descricao
27 | }
28 |
29 | api.post(`/entregas/${entregaId}/ocorrencias`, data)
30 | .then(response => {
31 | toast.success('Ocorrência adicionada ao pedido')
32 | onLoadOccurrences()
33 | onRequestClose()
34 | })
35 | .catch(response => {
36 | console.log(response.data)
37 | toast.error('Erro ao tentar salvar a ocorrência')
38 | })
39 | }
40 |
41 | return(
42 |
47 |
65 |
66 | )
67 | }
--------------------------------------------------------------------------------
/src/components/Modal/SearchCustomerModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, useState } from "react"
2 | import { FiChevronRight } from 'react-icons/fi'
3 |
4 | import { api } from "../../../services/api"
5 |
6 | import { Modal } from "../"
7 | import { Customer } from "../../../types"
8 |
9 | import { Input } from "../../../shared/Input"
10 | import { formatPhone } from "../../../utils/format"
11 |
12 | interface SearchCustomerModalProps {
13 | isOpen: boolean;
14 | onRequestClose: () => void;
15 | onCustomerSelected: (id: number, name: string) => void;
16 | }
17 |
18 | export function SearchCustomerModal({isOpen, onRequestClose, onCustomerSelected }: SearchCustomerModalProps) {
19 |
20 | const [customersFiltered, setCustomersFiltered] = useState([])
21 |
22 | function handleCustomerSelect(customer: Customer) {
23 | onCustomerSelected(customer.id, customer.nome)
24 | handleCloseModal()
25 | }
26 |
27 | function handleCloseModal() {
28 | setCustomersFiltered([])
29 |
30 | onRequestClose()
31 | }
32 |
33 | function handleSearchCustomer(event: ChangeEvent) {
34 | const searchBy = event.target.value
35 |
36 | if(searchBy.length > 2) {
37 | api.get(`/clientes?nome=${searchBy}`)
38 | .then(response => {
39 | const customers = response.data
40 | const customersFormatted = customers.map(customer => ({...customer, telefone: formatPhone(customer.telefone)}))
41 | setCustomersFiltered(customersFormatted)
42 | })
43 | }
44 | }
45 |
46 | return(
47 |
52 |
53 |
59 |
60 |
61 | {customersFiltered.map(customer => (
62 |
handleCustomerSelect(customer)}
66 | >
67 |
68 | {customer.nome}
69 | {customer.telefone}
70 |
71 |
72 |
73 | ))}
74 |
75 |
76 | )
77 | }
--------------------------------------------------------------------------------
/src/components/Modal/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import ReactModal from 'react-modal'
3 | import { FiX } from 'react-icons/fi'
4 |
5 | import { Header } from '../Header'
6 |
7 | interface ALModalProps {
8 | isOpen: boolean;
9 | onRequestClose: () => void;
10 | headerTitle: string;
11 | children: ReactNode;
12 | }
13 |
14 | ReactModal.setAppElement('#root')
15 |
16 | export function Modal( { isOpen, onRequestClose, headerTitle, children }: ALModalProps) {
17 |
18 | return(
19 |
25 |
26 |
27 |
28 | Fechar
29 |
30 |
31 | { children }
32 |
33 | )
34 | }
--------------------------------------------------------------------------------
/src/components/NavLink/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { Link, useRouteMatch } from "react-router-dom";
3 |
4 | interface NavLinkProps {
5 | to: string;
6 | label?: string;
7 | activeOnlyWhenExact?: boolean;
8 | children: ReactNode;
9 | }
10 |
11 | export function NavLink(props: NavLinkProps) {
12 | let match = useRouteMatch({
13 | path: props.to,
14 | exact: props.activeOnlyWhenExact
15 | });
16 |
17 | return (
18 |
19 | {props.children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/SideBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from '../Icon'
2 | import { NavLink } from '../NavLink';
3 | import { Menu, MenuItem } from "./styles";
4 |
5 | export function SideBar() {
6 | return(
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/src/components/SideBar/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | interface MenuItemProps {
4 | active?: Boolean;
5 | }
6 |
7 | export const Menu = styled.aside`
8 | background: var(--primary-color-light);
9 | width: var(--sidebar-width);
10 | position: fixed;
11 | left: 0;
12 | top: 0;
13 | z-index: 100;
14 | height: 100vh;
15 |
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | align-items: center;
20 |
21 | nav {
22 | width: 100%;
23 |
24 | ul {
25 | display: flex;
26 | flex-direction: column;
27 | gap: 0.25rem;
28 | }
29 | }
30 | `
31 |
32 | export const MenuItem = styled.li`
33 | height: 3.5rem;
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | position: relative;
38 |
39 | a {
40 | opacity: 0.6;
41 | display: inline-block;
42 | height: 100%;
43 | width: 100%;
44 |
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 |
49 | &:hover::before {
50 | content: "";
51 | position: absolute;
52 | top: 0;
53 | left: 0;
54 | width: 0.25rem;
55 | height: 100%;
56 | background: var(--background-color);
57 | border-radius: 0 0.3125rem 0.3125rem 0;
58 | }
59 |
60 | &.active {
61 | opacity: 1;
62 |
63 | &::before {
64 | content: "";
65 | position: absolute;
66 | top: 0;
67 | left: 0;
68 | width: 0.25rem;
69 | height: 100%;
70 | background: var(--background-color);
71 | border-radius: 0 0.3125rem 0.3125rem 0;
72 | }
73 | }
74 | }
75 | `
--------------------------------------------------------------------------------
/src/hooks/useCustomers.tsx:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from "axios";
2 | import { createContext, ReactNode, useContext, useEffect, useState } from "react";
3 | import { api } from "../services/api";
4 | import { Customer, CustomerInput } from "../types";
5 |
6 | interface CustomerContextData {
7 | customers: Customer[];
8 | customer: Customer;
9 | createCustomer: (customerInput: CustomerInput) => void;
10 | updateCustomer: (customer: Customer) => void;
11 | deleteCustomer: (customerId: number) => Promise;
12 | showCustomer: (customerId: number) => Promise
13 | }
14 |
15 | interface CustomerProviderProps {
16 | children: ReactNode;
17 | }
18 |
19 | const CustomersContext = createContext({} as CustomerContextData)
20 |
21 | export function CustomersProvider({ children }: CustomerProviderProps) {
22 |
23 | const [customers, setCustomers] = useState([])
24 | const [customer, setCustomer] = useState({} as Customer)
25 |
26 | useEffect(() => {
27 | api.get('/clientes')
28 | .then(response => setCustomers(response.data))
29 | }, [])
30 |
31 | async function showCustomer(id: number) {
32 | const response = await api.get(`/clientes/${id}`)
33 | setCustomer(response.data)
34 |
35 | return response.data
36 | }
37 |
38 | function createCustomer(customerInput: CustomerInput) {
39 | api.post>('/clientes', customerInput)
40 | .then(response => {
41 | const newCustomer = response.data
42 | let customersUpdate = [...customers]
43 |
44 | customersUpdate.push(newCustomer)
45 |
46 | setCustomer(newCustomer)
47 | setCustomers(customersUpdate)
48 | })
49 | .catch(error => {
50 | console.log(error.response.data)
51 | throw Error(error.response.data)
52 | })
53 | }
54 |
55 | async function updateCustomer(customer: Customer) {
56 | const response = await api.put(`/clientes/${customer.id}`, customer)
57 | const customerUpdated = response.data
58 |
59 | let customersUpdate = [...customers]
60 |
61 | const indexCustomerUpdated = customersUpdate.findIndex(customer => customer.id === customerUpdated.id)
62 | customersUpdate[indexCustomerUpdated] = customerUpdated
63 |
64 | setCustomer(customerUpdated)
65 | setCustomers(customersUpdate)
66 | }
67 |
68 | async function deleteCustomer(id: number) {
69 | await api.delete(`/clientes/${id}`)
70 |
71 | let customersUpdate = [...customers]
72 | customersUpdate = customersUpdate.filter(customer => customer.id !== id)
73 |
74 | setCustomers(customersUpdate)
75 |
76 | return;
77 | }
78 |
79 | return(
80 |
88 | {children}
89 |
90 | )
91 | }
92 |
93 | export function useCustomers() {
94 | const context = useContext(CustomersContext)
95 |
96 | return context
97 | }
--------------------------------------------------------------------------------
/src/hooks/useDeliveries.tsx:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from "axios";
2 | import { useState, createContext, ReactNode, useEffect, useContext } from "react";
3 | import { api } from "../services/api";
4 | import { Delivery, DeliveryInput } from "../types";
5 |
6 | interface DeliveriesContextData {
7 | deliveries: Delivery[];
8 | createDelivery: (delivery: DeliveryInput) => void;
9 | changeStatus: (deliveryId: number, newStatus: string) => void;
10 | }
11 |
12 | interface DeliveriesProviderProps {
13 | children: ReactNode;
14 | }
15 |
16 | const DeliveriesContext = createContext(
17 | {} as DeliveriesContextData
18 | )
19 |
20 | export function DeliveriesProvider({children}: DeliveriesProviderProps) {
21 | const [deliveries, setDeliveries] = useState([])
22 |
23 | useEffect(() => {
24 | api.get('/entregas')
25 | .then(response => setDeliveries(response.data))
26 | }, [])
27 |
28 | async function createDelivery(deliveryInput: DeliveryInput) {
29 | // api.post>('/entregas', deliveryInput)
30 | // .then()
31 | // .catch()
32 | const response = await api
33 | .post>('/entregas', deliveryInput)
34 |
35 | const delivery = response.data
36 | setDeliveries(oldState => [...oldState, delivery])
37 | }
38 |
39 | async function changeStatus(deliveryId: number, newStatus: string) {
40 | let deliveriesUpdate = [...deliveries]
41 | let deliveryUpdated = deliveriesUpdate.find(delivery => Number(delivery.id) === Number(deliveryId))
42 |
43 | if(deliveryUpdated) {
44 | switch (newStatus) {
45 | case 'FINALIZADA':
46 | await api.put(`/entregas/${deliveryId}/finalizacao`)
47 | deliveryUpdated.status = 'FINALIZADA'
48 |
49 | break;
50 |
51 | case 'CANCELADA':
52 | await api.put(`/entregas/${deliveryId}/cancelamento`)
53 | deliveryUpdated.status = 'CANCELADA'
54 |
55 | break;
56 |
57 | default:
58 | break;
59 | }
60 | }
61 | setDeliveries(deliveriesUpdate)
62 | }
63 |
64 | return(
65 |
72 | {children}
73 |
74 | );
75 | }
76 |
77 | export function useDeliveries() {
78 | const context = useContext(DeliveriesContext)
79 |
80 | return context
81 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { App} from './App';
4 |
5 | import { makeServer } from "./mirage/index";
6 |
7 | const environment = process.env.NODE_ENV;
8 |
9 | if (environment !== "production") {
10 | makeServer({ environment });
11 | }
12 |
13 | ReactDOM.render(
14 |
15 |
16 | ,
17 | document.getElementById('root')
18 | );
19 |
--------------------------------------------------------------------------------
/src/mirage/index.ts:
--------------------------------------------------------------------------------
1 | import { belongsTo, createServer, hasMany, Model, Response } from 'miragejs'
2 | import { Customer } from '../types';
3 |
4 | export function makeServer({ environment = 'test'}) {
5 | return createServer({
6 | environment,
7 |
8 | models: {
9 | cliente: Model,
10 | entrega: Model.extend({
11 | cliente: belongsTo(),
12 | ocorrencias: hasMany()
13 | }),
14 | ocorrencia: Model.extend({
15 | entrega: belongsTo()
16 | })
17 | },
18 |
19 | routes() {
20 | this.timing = 750
21 | this.namespace = 'api'
22 |
23 | this.get('/entregas', (schema) => {
24 | return schema.db.entregas
25 | })
26 |
27 | this.get('/entregas/:id', (schema, request) => {
28 | let entrega = schema.db.entregas.find(request.params.id)
29 | const cliente = schema.db.clientes.find(entrega.cliente.id)
30 |
31 | entrega.cliente = {
32 | id: cliente.id,
33 | nome: cliente.nome
34 | }
35 |
36 | return entrega
37 | })
38 |
39 | this.post('/entregas', (schema, request) => {
40 | const data = JSON.parse(request.requestBody)
41 | const cliente = schema.db.clientes.find(data.cliente.id)
42 |
43 | const newDelivery = {
44 | cliente: {
45 | id: cliente.id,
46 | nome: cliente.nome
47 | },
48 | taxa: data.taxa,
49 | destinatario: data.destinatario,
50 | status: 'PENDENTE',
51 | dataPedido: new Date(),
52 | dataFinalizacao: null,
53 | ocorrencias: []
54 | }
55 |
56 | return schema.db.entregas.insert(newDelivery)
57 | })
58 |
59 | this.put('/entregas/:id/finalizacao', (schema, request) => {
60 | const id = request.params.id
61 |
62 | schema.db.entregas
63 | .update(id, {
64 | status: 'FINALIZADA',
65 | dataFinalizacao: new Date()
66 | })
67 |
68 | return new Response(204)
69 | })
70 |
71 | this.put('/entregas/:id/cancelamento', (schema, request) => {
72 | const id = request.params.id
73 |
74 | schema.db.entregas
75 | .update(id, {
76 | status: 'CANCELADA',
77 | dataFinalizacao: null
78 | })
79 |
80 | return new Response(204)
81 | })
82 |
83 | this.get('/entregas/:id/ocorrencias', (schema, request) => {
84 | let entrega = schema.db.entregas.find(request.params.id)
85 | const ocorrencias = schema.db.ocorrencias.filter(ocorrencia => ocorrencia.entregaId === entrega.id)
86 |
87 | return ocorrencias
88 | })
89 |
90 | this.post('/entregas/:id/ocorrencias', (schema, request) => {
91 | let data = JSON.parse(request.requestBody)
92 |
93 | const newOcorrencia = {
94 | descricao: data.descricao,
95 | dataRegistro: new Date(),
96 | entregaId: request.params.id
97 | }
98 |
99 | return schema.db.ocorrencias.insert(newOcorrencia)
100 | })
101 |
102 | this.get('/clientes', (schema, request) => {
103 | const customers: Customer[] = schema.db.clientes
104 |
105 | if (!request.queryParams.nome) {
106 | return customers
107 | }
108 |
109 | const searchByNome = request.queryParams.nome
110 | return customers.filter(customer => customer.nome.toLowerCase().includes(searchByNome.toLowerCase()))
111 | })
112 |
113 | this.get('/clientes/:id', (schema, request) => {
114 | const id = request.params.id
115 | return schema.db.clientes.find(id)
116 | })
117 |
118 | this.post('/clientes', (schema, request) => {
119 | const data = JSON.parse(request.requestBody)
120 |
121 | return schema.db.clientes.insert(data)
122 | })
123 |
124 | this.put('/clientes/:id', (schema, request) => {
125 | const id = request.params.id
126 | const data = JSON.parse(request.requestBody)
127 |
128 | return schema.db.clientes
129 | .update(id, {id, ...data})
130 | })
131 |
132 | this.delete('/clientes/:id', (schema, request) => {
133 | const id = request.params.id
134 | const deliveries = schema.db.entregas
135 | const deliveriesFiltered = deliveries.filter(delivery => delivery.cliente.id === id)
136 |
137 | if(deliveriesFiltered.length > 0) {
138 | return new Response(400, { some: 'header' }, { errors: [`O cliente não pode ser excluído`] })
139 | }
140 |
141 | schema.db.clientes.remove(id)
142 |
143 | return new Response(204)
144 | })
145 | },
146 |
147 | seeds(server) {
148 | server.db.loadData({
149 | clientes: [
150 | {
151 | id: 1,
152 | nome: 'Glauber de Oliveira Matos',
153 | email: 'glaub.oliveira@hotmail.com',
154 | telefone: '73981787390'
155 | },
156 | {
157 | id: 2,
158 | nome: 'Mariah Fátima Sueli Novaes',
159 | email: 'mariahfatimasuelinovaes@mrv.com.br',
160 | telefone: '1585428107'
161 | },
162 | {
163 | id: 3,
164 | nome: 'Hugo Elias Guilherme Rezende',
165 | email: 'hugoeliasguilhermerezende@clickfios.com.br',
166 | telefone: '61985260763'
167 | }
168 | ],
169 | entregas: [
170 | {
171 | id: 1,
172 | cliente: {
173 | id: 1,
174 | nome: 'Glauber de Oliveira Matos'
175 | },
176 | destinatario: {
177 | nome: 'Glauber',
178 | logradouro: 'Rua Governador Valdir Pires',
179 | numero: 'S/Nº',
180 | complemento: 'Casa',
181 | bairro: 'Centro'
182 | },
183 | taxa: 15,
184 | status: 'PENDENTE',
185 | dataPedido: new Date(),
186 | dataFinalizacao: null
187 | },
188 | {
189 | id: 2,
190 | cliente: {
191 | id: 1,
192 | nome: 'Glauber de Oliveira Matos'
193 | },
194 | destinatario: {
195 | nome: 'Glauber',
196 | logradouro: 'Rua Governador Valdir Pires',
197 | numero: 'S/Nº',
198 | complemento: 'Casa',
199 | bairro: 'Centro'
200 | },
201 | taxa: 25,
202 | status: 'FINALIZADA',
203 | dataPedido: new Date(),
204 | dataFinalizacao: new Date()
205 | },
206 | {
207 | id: 3,
208 | cliente: {
209 | id: 1,
210 | nome: 'Glauber de Oliveira Matos'
211 | },
212 | destinatario: {
213 | nome: 'Glauber',
214 | logradouro: 'Rua Governador Valdir Pires',
215 | numero: 'S/Nº',
216 | complemento: 'Casa',
217 | bairro: 'Centro'
218 | },
219 | taxa: 10,
220 | status: 'CANCELADA',
221 | dataPedido: new Date(),
222 | dataFinalizacao: null
223 | }
224 | ],
225 | ocorrencias: []
226 | })
227 | }
228 | })
229 | }
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Switch } from "react-router";
2 | import { CreateNewDelivery } from "./view/CreateNewDeliveryView";
3 | import { Customers } from "./view/CustomersView";
4 | import { Dashboard } from "./view/DashboardView";
5 | import { Deliveries } from "./view/DeliveriesView";
6 | import { DeliveryDetails } from "./view/DeliveryDetailsView";
7 | import { RegistrationCustomer } from "./view/RegistrationCustomerView";
8 |
9 | export function Routes() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const isDevelopment = process.env.NODE_ENV !== 'production'
4 |
5 | export const api = axios.create({
6 | baseURL: isDevelopment ? 'http://localhost:3000/api' : 'https://algalog-api-glauber.herokuapp.com/'
7 | })
--------------------------------------------------------------------------------
/src/shared/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | import { Container } from "./styles";
4 |
5 | interface ButtonProps extends React.ButtonHTMLAttributes {
6 | color?: 'primary' | 'default' | 'danger'
7 | onClick?: () => void;
8 | children?: ReactNode;
9 | }
10 |
11 | export function Button({color = 'default', onClick, children, ...props}: ButtonProps) {
12 | return(
13 |
18 | {children || 'nameless'}
19 |
20 | );
21 | }
--------------------------------------------------------------------------------
/src/shared/Button/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 |
3 | interface ButtonProps {
4 | color: string
5 | }
6 |
7 | export const Container = styled.button`
8 |
9 | padding: 0 1rem;
10 | text-transform: uppercase;
11 | letter-spacing: 0.07875rem; /*1.26px = 9%*/
12 | font: 600 0.9375rem "Inter", sans-serif; /*14px*/
13 | border-radius: 0.5rem;
14 |
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | transition: filter 0.3s;
19 |
20 | &:hover:not(:disabled) {
21 | filter: brightness(0.9);
22 | }
23 |
24 | ${props => {
25 | switch (props.color) {
26 | case 'primary':
27 | return css`
28 | background: var(--primary-color-light);
29 | color: var(--background-color);
30 | `
31 | case 'danger':
32 | return css`
33 | background: var(--danger-color);
34 | color: var(--background-color);
35 | `
36 |
37 | default:
38 | return css`
39 | background: var(--body-color);
40 | color: var(--primary-color-light);
41 | `;
42 | }
43 | }}
44 |
45 | /* &.primary-light {
46 | background: var(--primary-color-light);
47 | color: var(--background-color);
48 | }
49 |
50 | &.default {
51 | background: var(--body-color);
52 | color: var(--primary-color-light);
53 | }
54 |
55 | &.danger {
56 | background: var(--danger-color);
57 | color: var(--background-color);
58 | } */
59 |
60 | svg {
61 | margin-right: 1rem;
62 | }
63 | `
--------------------------------------------------------------------------------
/src/shared/Input/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { FormGroup } from "../../styles/global";
3 | import InputMask from 'react-input-mask'
4 |
5 | interface InputProps extends React.InputHTMLAttributes {
6 | label: string;
7 | addOn?: ReactNode;
8 | mask?: string | RegExp[] | undefined;
9 | }
10 |
11 | export function Input({label, addOn, mask, ...rest}: InputProps) {
12 |
13 | function formatLabel(): string | undefined{
14 | let labelFormated = label?.toLowerCase().replace(' ', '')
15 | return labelFormated || undefined;
16 | }
17 |
18 |
19 | return(
20 |
21 | {label}
22 | {addOn ? (
23 |
24 |
25 | {addOn}
26 |
27 | {mask ? (
28 |
33 | ) : (
34 |
38 | )}
39 |
40 | ) : (
41 | mask ? (
42 |
47 | ) : (
48 |
52 | )
53 | )}
54 |
55 | );
56 | }
--------------------------------------------------------------------------------
/src/shared/Textarea/index.tsx:
--------------------------------------------------------------------------------
1 | import { FormGroup } from "../../styles/global";
2 |
3 | interface InputProps extends React.TextareaHTMLAttributes {
4 | label: string;
5 | }
6 |
7 | export function Textarea({label, ...rest}: InputProps) {
8 |
9 | function formatLabel(): string | undefined{
10 | let labelFormated = label?.toLowerCase().replace(' ', '')
11 | return labelFormated || undefined;
12 | }
13 |
14 | return(
15 |
16 | {label}
17 |
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/src/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 | import styled from 'styled-components'
3 |
4 | export const GlobalStyle = createGlobalStyle`
5 | :root {
6 | /*Colors HUE*/
7 | --hue: 128;
8 |
9 | --hue-status-pedido-cancelado: 358;
10 | --hue-status-pedido-pendente: 209;
11 | --hue-status-pedido-finalizado: 128;
12 | /*===================================*/
13 |
14 | --body-color: hsla(var(--hue), 9%, 95%, 1);
15 | --background-color: #ffffff;
16 |
17 | --primary-color: hsla(var(--hue), 44%, 44%, 1);
18 | --primary-color-light: hsla(var(--hue), 39%, 54%, 1);
19 | --primary-color-extra-light: hsla(var(--hue), 33%, 96%, 1);
20 | --danger-color: hsla(var(--hue-status-pedido-cancelado), 55%, 52%, 1);
21 |
22 | --input-border-color: hsla(var(--hue), 12%, 80%, 1);
23 | --input-border-color-focus: hsla(var(--hue), 22%, 60%, 1);
24 |
25 | --gray-800: hsla(var(--hue), 80%, 0%, 0.8);
26 | --gray-600: hsla(var(--hue), 80%, 0%, 0.6);
27 | --gray-500: hsla(var(--hue), 80%, 0%, 0.5);
28 | --gray-400: hsla(var(--hue), 80%, 0%, 0.4);
29 | --gray-300: hsla(var(--hue), 80%, 0%, 0.3);
30 | --gray-200: hsla(var(--hue), 80%, 0%, 0.2);
31 | --gray-100: hsla(var(--hue), 80%, 0%, 0.1);
32 |
33 | --box-shadow-color: hsla(var(--hue), 49%, 34%, 0.05);
34 |
35 | /*Status dos Pedidos*/
36 | /*PENDENTE*/
37 | --status-pedido-pendente: hsla(var(--hue-status-pedido-pendente), 55%, 52%, 1);
38 | --status-pedido-pendente-light: hsla(var(--hue-status-pedido-pendente), 55%, 52%, 0.1 );
39 |
40 | /*CANCELADO*/
41 | --status-pedido-cancelado: hsla(var(--hue-status-pedido-cancelado), 62%, 50%, 1);
42 | --status-pedido-cancelado-light: hsla(var(--hue-status-pedido-cancelado), 62%, 50%, 0.1);
43 |
44 | /*FINALIZADO*/
45 | --status-pedido-finalizado: hsla(var(--hue-status-pedido-finalizado), 44%, 44%, 1);
46 | --status-pedido-finalizado-light: hsla(var(--hue-status-pedido-finalizado), 44%, 44%, 0.1);
47 |
48 |
49 | --sidebar-width: 5.625rem;
50 | --compenents-form-height: 3rem
51 | }
52 |
53 | * {
54 | padding: 0;
55 | margin: 0;
56 | box-sizing: border-box;
57 | }
58 |
59 | html {
60 | @media (max-width: 1080px) {
61 | font-size: 93.75%;
62 | }
63 | @media (max-width: 720px) {
64 | font-size: 87.5%;
65 | }
66 | }
67 |
68 | body {
69 | background: var(--body-color);
70 | -webkit-font-smoothing: antialiased;
71 | }
72 |
73 | body, input, textarea, button {
74 | font-family: 'Inter', sans-serif;
75 | font-weight: 400;
76 | }
77 |
78 | a {
79 | display: inline-flex;
80 | text-decoration: none;
81 | }
82 |
83 | button {
84 | border: 0;
85 | cursor: pointer;
86 | }
87 |
88 | input,
89 | button {
90 | height: var(--compenents-form-height);
91 | }
92 |
93 | [disabled] {
94 | opacity: 0.5;
95 | cursor: not-allowed;
96 | }
97 |
98 | .link-button {
99 | font: 500 0.9375rem 'Inter', sans-serif;
100 | line-height: 1.09375rem;
101 | color: var(--gray-800);
102 | text-transform: uppercase;
103 | text-decoration: none;
104 | letter-spacing: 0.07875rem;
105 | height: 2rem;
106 |
107 | background: none;
108 |
109 | display: flex;
110 | align-items: center;
111 |
112 | transition: opacity 0.2s;
113 |
114 | &:hover {
115 | opacity: 0.6;
116 | text-decoration: underline;
117 | }
118 |
119 | svg {
120 | stroke: var(--gray-800);
121 | margin-right: 0.75rem;
122 | }
123 | }
124 |
125 | form {
126 | display: inline-block;
127 | width: 100%;
128 |
129 | h2 {
130 | text-transform: uppercase;
131 | font: 400 1rem "Inter", sans-serif;
132 | color: var(--gray-600);
133 | margin: 2rem 0 1.5rem 0;
134 | }
135 |
136 | /* .form-group {
137 | display: flex;
138 | flex-direction: column;
139 |
140 | & + .form-group {
141 | margin-top: 1.5rem;
142 | }
143 |
144 | label {
145 | font: 400 0.9375rem 'Inter', sans-serif;
146 | line-height: 1.171875rem;
147 | color: var(--gray-500);
148 | margin-bottom: 0.5rem;
149 | }
150 |
151 | .input-group {
152 | display: flex;
153 | justify-content: flex-start;
154 |
155 | .input-group-addon {
156 | border-radius: 0.5rem 0 0 0.5rem;
157 | background: var(--background-color);
158 | border: 1.5px solid var(--input-border-color);
159 | color: var(--gray-600);
160 | padding: 0 1rem;
161 |
162 | display: flex;
163 | align-items: center;
164 |
165 | svg {
166 | opacity: 0.6;
167 | }
168 | }
169 |
170 | input {
171 | flex: 1;
172 | border-radius: 0 0.5rem 0.5rem 0;
173 | border-left: 1.5px solid transparent;
174 | }
175 | }
176 |
177 | input, textarea {
178 | font: 400 1rem 'Inter', sans-serif;
179 | padding: 0 1rem;
180 | border-radius: 0.5rem;
181 | border: 1.5px solid var(--input-border-color);
182 | background: var(--background-color);
183 | color: var(--gray-800);
184 | outline: none;
185 |
186 | &::placeholder {
187 | color: var(--gray-300);
188 | }
189 |
190 | &:hover {
191 | border: 1.5px solid var(--input-border-color-focus);
192 | }
193 |
194 | &:focus {
195 | border: 1.5px solid var(--input-border-color-focus);
196 | box-shadow: 2px 3px 4px var(--gray-100);
197 | }
198 | }
199 |
200 | textarea {
201 | padding-top: 0.75rem;
202 | padding-bottom: 0.75rem;
203 | }
204 | } */
205 |
206 | .form-actions {
207 | margin-top: 2.5rem;
208 | display: flex;
209 | gap: 1rem;
210 | }
211 | }
212 |
213 | .react-modal-overlay {
214 | background: rgba(0, 0, 0, 0.5);
215 | position: fixed;
216 | top: 0;
217 | right: 0;
218 | bottom: 0;
219 | left: 0;
220 |
221 | z-index: 200;
222 |
223 | display: flex;
224 | justify-content: flex-end;
225 | }
226 |
227 | .react-modal-content {
228 | height: 100%;
229 | width: 85%;
230 | max-width: 35rem;
231 | background: var(--background-color);
232 | padding: 4rem 3rem;
233 |
234 | transform: translateX(35rem);
235 | transition: all 400ms ease;
236 | }
237 |
238 | .ReactModal__Content--after-open {
239 | transform: translateX(0);
240 | }
241 |
242 | .ReactModal__Overlay {
243 | opacity: 0;
244 | transition: all 500ms ease-in-out;
245 | }
246 |
247 | .ReactModal__Overlay--after-open {
248 | opacity: 1;
249 | }
250 |
251 | .ReactModal__Overlay--before-close {
252 | opacity: 0;
253 | }
254 |
255 | .client-list {
256 | margin-top: 2.5rem;
257 |
258 | .client-list__item {
259 | cursor: pointer;
260 | padding-bottom: 1.5rem;
261 | display: flex;
262 | align-items: center;
263 | justify-content: space-between;
264 |
265 | opacity: 0.75;
266 | transition: opacity 0.3s;
267 |
268 | &:hover {
269 | opacity: 1;
270 | }
271 |
272 | & + .client-list__item {
273 | border-top: 1px solid var(--gray-100);
274 | padding-top: 1.5rem;
275 | }
276 | }
277 |
278 | .client-list__item-info {
279 | display: flex;
280 | flex-direction: column;
281 | gap: 0.25rem;
282 |
283 | strong {
284 | color: var(--gray-600);
285 | }
286 |
287 | span {
288 | color: var(--gray-500);
289 | }
290 | }
291 | }
292 | `
293 |
294 | export const FormGroup = styled.div`
295 | display: flex;
296 | flex-direction: column;
297 |
298 | & + & {
299 | margin-top: 1.5rem;
300 | }
301 |
302 | label {
303 | font: 400 0.9375rem 'Inter', sans-serif;
304 | line-height: 1.171875rem;
305 | color: var(--gray-500);
306 | margin-bottom: 0.5rem;
307 | }
308 |
309 | .input-group {
310 | display: flex;
311 | justify-content: flex-start;
312 |
313 | .input-group-addon {
314 | border-radius: 0.5rem 0 0 0.5rem;
315 | background: var(--background-color);
316 | border: 1.5px solid var(--input-border-color);
317 | color: var(--gray-600);
318 | padding: 0 1rem;
319 |
320 | display: flex;
321 | align-items: center;
322 |
323 | svg {
324 | opacity: 0.6;
325 | }
326 | }
327 |
328 | input {
329 | flex: 1;
330 | border-radius: 0 0.5rem 0.5rem 0;
331 | border-left: 1.5px solid transparent;
332 | }
333 | }
334 |
335 | input, textarea {
336 | font: 400 1rem 'Inter', sans-serif;
337 | padding: 0 1rem;
338 | border-radius: 0.5rem;
339 | border: 1.5px solid var(--input-border-color);
340 | background: var(--background-color);
341 | color: var(--gray-800);
342 | outline: none;
343 |
344 | &::placeholder {
345 | color: var(--gray-300);
346 | }
347 |
348 | &:hover {
349 | border: 1.5px solid var(--input-border-color-focus);
350 | }
351 |
352 | &:focus {
353 | border: 1.5px solid var(--input-border-color-focus);
354 | box-shadow: 2px 3px 4px var(--gray-100);
355 | }
356 | }
357 |
358 | textarea {
359 | padding-top: 0.75rem;
360 | padding-bottom: 0.75rem;
361 | }
362 | `
363 |
364 | export const PageContent = styled.section`
365 | margin-top: 3rem;
366 | `
367 |
368 | export const Title = styled.h1`
369 | font: 400 2rem 'Nunito', sans-serif;
370 | line-height: 2.5rem;
371 | color: var(--gray-600);
372 | `
--------------------------------------------------------------------------------
/src/styles/view/create-new-delivery.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../global";
3 |
4 | export const Container = styled.div``
5 |
6 | export const Content = styled(PageContent)`
7 | background: var(--background-color);
8 | border-radius: 0.75rem;
9 |
10 | box-shadow: 0px 4px 20px var(--box-shadow-color);
11 |
12 | padding: 2.5rem;
13 |
14 | /* devices: > 1024 */
15 | @media (min-width: 1024px) {
16 | .form-inline {
17 | display: grid;
18 | grid-template-columns: calc(50% - 0.5rem) calc(50% - 0.5rem);
19 | column-gap: 1rem;
20 | row-gap: 1.5rem;
21 |
22 | .form-group + .form-group {
23 | margin-top: 0;
24 | }
25 | }
26 | }
27 |
28 | /* devices: > 1200 */
29 | @media (min-width: 1200px) {
30 | .form-inline {
31 | grid-template-columns:
32 | calc(30% - 1rem) calc(20% - 1rem) calc(20% - 1rem)
33 | 30%;
34 | column-gap: 1rem;
35 |
36 | .form-group + .form-group {
37 | margin-top: 0;
38 | }
39 | }
40 | }
41 | `
--------------------------------------------------------------------------------
/src/styles/view/customers.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../global";
3 |
4 | export const Container = styled.div``
5 |
6 | export const Content = styled(PageContent)`
7 |
8 | table {
9 | width: 100%;
10 | border-spacing: 0 0.5rem;
11 | text-align: left;
12 | color: var(--gray-600);
13 |
14 | thead tr th {
15 | padding: 0.5rem 2rem;
16 | font: 700 0.875rem 'Nunito', sans-serif;
17 | text-transform: uppercase;
18 | line-height: 0.9375rem;
19 | color: var(--gray-500);
20 | }
21 |
22 | tbody {
23 | background: var(--background-color);
24 |
25 | tr {
26 | opacity: 0.8;
27 | box-shadow: 0px 4px 20px var(--box-shadow-color);
28 |
29 | &:hover {
30 | opacity: 1;
31 | background: linear-gradient(
32 | 95deg,
33 | var(--primary-color-light) -220%,
34 | rgba(255, 255, 255, 1) 45%
35 | );
36 |
37 | & td {
38 | border-top-color: var(--primary-color-light);
39 | border-bottom-color: var(--primary-color-light);
40 | }
41 |
42 | & td:first-child {
43 | border-left: 4px solid var(--primary-color-light);
44 | }
45 |
46 | & td:last-child {
47 | border-right-color: var(--primary-color-light);
48 | }
49 | }
50 |
51 | td {
52 | padding: 1rem 2rem;
53 | border-top: 1px solid transparent;
54 | border-bottom: 1px solid transparent;
55 |
56 | &:first-child {
57 | padding-left: calc(2rem - 4px);
58 | border-left: 4px solid transparent;
59 | border-radius: 0.5rem 0 0 0.5rem;
60 | font: 600 1rem 'Inter', sans-serif;
61 | color: var(--gray-600);
62 | }
63 |
64 | &:last-child {
65 | border-right: 1px solid transparent;
66 | border-radius: 0 0.5rem 0.5rem 0;
67 | text-align: right;
68 | }
69 |
70 | button {
71 | width: 3rem;
72 | height: 3rem;
73 | background: var(--body-color);
74 | border: 0;
75 | border-radius: 0.75rem;
76 |
77 | transition: filter 0.2s;
78 |
79 | & + button {
80 | margin-left: 0.75rem;
81 | }
82 |
83 | &:hover {
84 | filter: brightness(0.96);
85 | }
86 | }
87 | }
88 |
89 | .edit {
90 | stroke: var(--primary-color-light);
91 | }
92 |
93 | .trash {
94 | stroke: var(--danger-color);
95 | }
96 | }
97 | }
98 | }
99 |
100 | .modal-delete-cliente {
101 | display: flex;
102 | flex-direction: column;
103 | align-items: center;
104 |
105 | svg {
106 | stroke: var(--danger-color);
107 | }
108 |
109 | span {
110 | text-transform: uppercase;
111 | margin-top: 2rem;
112 | margin-bottom: 1.5rem;
113 | font: 500 1rem 'Inter', sans-serif;
114 | color: var(--gray-800);
115 | }
116 |
117 | p {
118 | margin-bottom: 3rem;
119 | font: 500 1.125rem 'Inter', sans-serif;
120 | line-height: 1.40625rem;
121 | max-width: 19rem;
122 | text-align: center;
123 | color: var(--gray-400);
124 | }
125 |
126 | .modal-actions {
127 | display: flex;
128 | justify-content: center;
129 | align-items: center;
130 | gap: 1rem;
131 |
132 | button.default {
133 | color: var(--danger-color);
134 | }
135 | }
136 | }
137 |
138 | `
--------------------------------------------------------------------------------
/src/styles/view/dashboard.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../global";
3 |
4 | export const Container = styled.div``
5 |
6 | export const Content = styled(PageContent)`
7 | display: grid;
8 | grid-template-columns: repeat(2, 1fr);
9 | gap: 2.5rem;
10 |
11 | .card {
12 | background: var(--background-color);
13 | border-radius: 0.5rem;
14 | padding: 2rem 2rem 2rem 1.5rem;
15 | border: 1px solid transparent;
16 | box-shadow: 0px 4px 20px var(--box-shadow-color);
17 | transition: 0.2s;
18 |
19 | &:hover {
20 | border: 1px solid var(--primary-color-light);
21 | background: linear-gradient(
22 | 95deg,
23 | var(--primary-color-light) -220%,
24 | rgba(255, 255, 255, 1) 45%
25 | );
26 | transform: translateY(-0.5rem);
27 | }
28 |
29 | a {
30 | text-decoration: none;
31 | display: flex;
32 |
33 | .img-mask {
34 | background: var(--primary-color-extra-light);
35 | border-radius: 0.75rem;
36 | padding: 1rem;
37 | }
38 |
39 | .card-info {
40 | flex: 1;
41 | margin-left: 2rem;
42 |
43 | strong {
44 | font: 700 2.25rem 'Inter', sans-serif;
45 | color: var(--primary-color-light);
46 | }
47 | }
48 |
49 | .card-info-title {
50 | font: 400 1.75rem 'Nunito', sans-serif;
51 | color: var(--gray-500);
52 | margin-bottom: 0.25rem;
53 | }
54 |
55 | svg {
56 | align-self: center;
57 | stroke: var(--gray-300);
58 | }
59 | }
60 | }
61 |
62 | `
--------------------------------------------------------------------------------
/src/styles/view/deliveries.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../global";
3 |
4 | export const Container = styled.div``
5 |
6 | export const Content = styled(PageContent)`
7 | /* .tool-bar {
8 | display: flex;
9 | justify-content: flex-start;
10 | margin-bottom: 1rem;
11 | } */
12 | `
13 |
14 | export const DeliveriesContainer = styled.div`
15 | display: flex;
16 | flex-direction: column;
17 | gap: 2rem;
18 | margin-bottom: 2rem;
19 |
20 | .card {
21 | width: 100%;
22 | background: var(--background-color);
23 | border-radius: 0.5rem;
24 |
25 | box-shadow: 0px 4px 20px var(--box-shadow-color);
26 | border: 1px solid transparent;
27 | border-left: 4px solid transparent;
28 |
29 | &:hover {
30 | border-color: var(--primary-color-light);
31 | background: linear-gradient(
32 | 95deg,
33 | var(--primary-color-light) -220%,
34 | rgba(255, 255, 255, 1) 45%
35 | );
36 | transition: 0.3s;
37 | }
38 |
39 | a {
40 | display: block;
41 | text-decoration: none;
42 | padding: 1.5rem 2rem 2rem 2rem;
43 | }
44 |
45 | .header {
46 | margin: -1.5rem -2rem 0 -2rem;
47 | padding: 1.5rem 2rem;
48 | border-bottom: 1px solid var(--gray-100);
49 |
50 | border-radius: 0.75rem 0.75rem 0 0;
51 | filter: brightness(0.99);
52 |
53 | display: flex;
54 | justify-content: space-between;
55 | align-items: center;
56 |
57 | > div {
58 | display: flex;
59 | flex-direction: column;
60 | }
61 |
62 | span {
63 | font: 400 0.75rem "Nunito", sans-serif;
64 | text-transform: uppercase;
65 | letter-spacing: 0.075rem;
66 | line-height: 0.9375rem; /*tamanho da font * 1.25 = 15px*/
67 | color: var(--gray-600);
68 | margin-bottom: 0.5rem;
69 | }
70 |
71 | strong {
72 | font: 600 1.125rem "Inter", sans-serif;
73 | color: var(--gray-600);
74 | }
75 |
76 | .status {
77 | padding: 0.25rem 0.75rem;
78 | border-radius: 0.5rem;
79 | font: 700 0.75rem "Inter", sans-serif;
80 | letter-spacing: 0.07875rem; /*9%*/
81 | text-transform: uppercase;
82 |
83 | &.pendente {
84 | border: 1px solid var(--status-pedido-pendente);
85 | background: var(--status-pedido-pendente-light);
86 | color: var(--status-pedido-pendente);
87 | }
88 |
89 | &.cancelada {
90 | border: 1px solid var(--status-pedido-cancelado);
91 | background: var(--status-pedido-cancelado-light);
92 | color: var(--status-pedido-cancelado);
93 | }
94 |
95 | &.finalizada {
96 | border: 1px solid var(--status-pedido-finalizado);
97 | background: var(--status-pedido-finalizado-light);
98 | color: var(--status-pedido-finalizado);
99 | }
100 | }
101 | }
102 |
103 | .body {
104 | padding-top: 2rem;
105 | display: flex;
106 | justify-content: space-between;
107 | align-items: flex-start;
108 |
109 | label {
110 | text-transform: uppercase;
111 | color: var(--gray-400);
112 | margin-bottom: 0.5rem;
113 | }
114 |
115 | p {
116 | font-weight: 600;
117 | color: var(--gray-600);
118 | }
119 |
120 | strong {
121 | font-size: 1.5rem;
122 | color: var(--gray-600);
123 | }
124 |
125 | .cliente {
126 | display: flex;
127 | flex-direction: column;
128 | }
129 |
130 | .taxa {
131 | display: flex;
132 | flex-direction: column;
133 | text-align: right;
134 | }
135 | }
136 | }
137 |
138 | @media (min-width: 980px) {
139 | & {
140 | display: grid;
141 | grid-template-columns: repeat(2, 1fr);
142 | gap: 2rem;
143 | }
144 | }
145 | `
--------------------------------------------------------------------------------
/src/styles/view/delivery-details.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../global";
3 |
4 | export const Container = styled.div``
5 |
6 | export const Content = styled(PageContent)`
7 | background: var(--background-color);
8 | border-radius: 0.75rem;
9 |
10 | box-shadow: 0px 4px 20px var(--box-shadow-color);
11 |
12 | padding: 0 2.5rem 2.5rem 2.5rem;
13 |
14 | .header {
15 | margin: 0 -2.5rem 0 -2.5rem;
16 | border-radius: 0.75rem 0.75rem 0 0;
17 | padding: 2rem 3rem 1.5rem 3rem;
18 | border-bottom: 1px solid var(--gray-100);
19 | background: var(--background-color);
20 | filter: brightness(0.99);
21 |
22 | display: grid;
23 | grid-template-columns: repeat(4, 1fr);
24 | align-items: center;
25 |
26 | div {
27 | display: flex;
28 | flex-direction: column;
29 | gap: 0.25rem;
30 |
31 | span {
32 | text-transform: uppercase;
33 | font: 400 0.75rem "Nunito", sans-serif;
34 | letter-spacing: 0.075rem;
35 | color: var(--gray-500);
36 | }
37 |
38 | strong {
39 | font: 500 1.25rem "Inter", sans-serif;
40 | color: var(--gray-800);
41 | }
42 | }
43 |
44 | select {
45 | height: 2.5rem;
46 | font: 600 0.9375rem "Inter", sans-serif;
47 | text-transform: uppercase;
48 | border-radius: 0.5rem;
49 | padding: 0 1rem;
50 | }
51 |
52 | select.pendente {
53 | background: var(--status-pedido-pendente-light);
54 | border: 1.5px solid var(--status-pedido-pendente);
55 | color: var(--status-pedido-pendente)
56 | }
57 |
58 | select.cancelada {
59 | background: var(--status-pedido-cancelado-light);
60 | border: 1.5px solid var(--status-pedido-cancelado);
61 | color: var(--status-pedido-cancelado)
62 | }
63 |
64 | select.finalizada {
65 | background: var(--status-pedido-finalizado-light);
66 | border: 1.5px solid var(--status-pedido-finalizado);
67 | color: var(--status-pedido-finalizado)
68 | }
69 |
70 | select option {
71 | background: var(--background-color);
72 | color: var(--gray-600);
73 | }
74 |
75 | }
76 |
77 | .body {
78 | margin-top: 3rem;
79 | display: flex;
80 | justify-content: space-between;
81 |
82 | .dados {
83 | width: 100%;
84 | margin-right: 3rem;
85 | }
86 |
87 | .separator {
88 | border-bottom: 1px solid var(--gray-100);
89 | display: flex;
90 | justify-content: space-between;
91 |
92 | h3 {
93 | display: inline;
94 | font: 400 1rem "Nunito", sans-serif;
95 | color: var(--gray-800);
96 | text-transform: uppercase;
97 | letter-spacing: 0.1rem;
98 | padding-bottom: 0.5rem;
99 | border-bottom: 1px solid var(--gray-300);
100 | }
101 | }
102 |
103 | .box-separator {
104 | border-right: 1px solid var(--gray-100);
105 | margin-right: -3rem;
106 | margin-top: 2rem;
107 | padding-right: 3rem;
108 | }
109 |
110 | .endereco .separator {
111 | margin-top: 3rem;
112 | margin-bottom: 2rem;
113 | }
114 |
115 | .ocorrencias {
116 | width: 100%;
117 | margin-left: 3rem;
118 |
119 | .ocorrencias-total {
120 | display: inline-block;
121 | color: var(--gray-600);
122 | }
123 |
124 | .actions {
125 | margin-top: 1.5rem;
126 | margin-bottom: 2rem;
127 | display: flex;
128 | justify-content: flex-end;
129 | }
130 |
131 | ul {
132 | list-style: none;
133 |
134 | li {
135 | padding: 1rem 1.5rem;
136 | border-radius: 0.5rem;
137 |
138 | & + li {
139 | margin-top: 0.5rem;
140 | }
141 |
142 | &.finalizado {
143 | border: 1px solid var(--status-pedido-finalizado-light);
144 | border-left: 0.375rem solid var(--status-pedido-finalizado);
145 | }
146 |
147 | &.danger {
148 | border: 1px solid var(--status-pedido-cancelado-light);
149 | border-left: 0.375rem solid var(--danger-color);
150 | }
151 |
152 | strong {
153 | display: block;
154 | font: 400 1rem "Inter", sans-serif;
155 | color: var(--gray-800);
156 | margin-bottom: 0.5rem;
157 | }
158 |
159 | span {
160 | font: 400 0.875rem "Inter", sans-serif;
161 | color: var(--gray-600);
162 | }
163 | }
164 | }
165 | }
166 |
167 | .input-group {
168 | display: flex;
169 | flex-direction: column;
170 | gap: 0.75rem;
171 |
172 | & + .input-group {
173 | margin-top: 1.5rem;
174 | }
175 |
176 | span {
177 | color: var(--gray-300);
178 | }
179 |
180 | p {
181 | color: var(--gray-800);
182 | }
183 | }
184 |
185 | .inline {
186 | display: flex;
187 | gap: 3rem;
188 |
189 | .input-group {
190 | margin-top: 1.5rem;
191 | }
192 | }
193 | }
194 | `
--------------------------------------------------------------------------------
/src/styles/view/registration-customer.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { PageContent } from "../global";
3 |
4 | export const Container = styled.div``
5 |
6 | export const Content = styled(PageContent)`
7 | background: var(--background-color);
8 | border-radius: 0.75rem;
9 |
10 | box-shadow: 0px 4px 20px var(--box-shadow-color);
11 |
12 | padding: 2.5rem;
13 |
14 | .form-inline {
15 | margin-top: 1.5rem;
16 | }
17 |
18 | /* devices: > 1024 */
19 | @media (min-width: 1024px) {
20 | .form-inline {
21 | display: grid;
22 | grid-template-columns: 1fr 1fr;
23 | column-gap: 1rem;
24 |
25 | .form-group + .form-group {
26 | margin-top: 0;
27 | }
28 | }
29 | }
30 | `
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface Customer {
2 | id: number;
3 | nome: string;
4 | email: string;
5 | telefone: string;
6 | }
7 |
8 | export interface CustomerInput {
9 | nome: string;
10 | email: string;
11 | telefone: string;
12 | }
13 |
14 | export interface Delivery {
15 | id: number,
16 | cliente: {
17 | id: number,
18 | nome: string
19 | },
20 | destinatario: {
21 | nome: string,
22 | logradouro: string,
23 | numero: string,
24 | complemento: string,
25 | bairro: string
26 | },
27 | taxa: number,
28 | status: 'FINALIZADA' | 'PENDENTE' | 'CANCELADA',
29 | dataPedido: string,
30 | dataFinalizacao: string
31 | }
32 |
33 | export interface DeliveryInput {
34 | cliente: {
35 | id: number
36 | },
37 | destinatario: {
38 | nome: string,
39 | logradouro: string,
40 | numero: string,
41 | complemento: string,
42 | bairro: string
43 | },
44 | taxa: number
45 | }
46 |
47 | export interface Occurrence {
48 | id: number;
49 | descricao: string;
50 | dataRegistro: string;
51 | }
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | export const { format: formatPrice } = new Intl.NumberFormat('pt-BR', {
2 | style: 'currency',
3 | currency: 'BRL'
4 | })
5 |
6 | export const { format: formatDateTime } = new Intl.DateTimeFormat('default', {
7 | year: 'numeric', month: 'numeric', day: 'numeric',
8 | hour: 'numeric', minute: 'numeric',
9 | hour12: false,
10 | })
11 |
12 | export const formatPhone = (phone: string) => {
13 | var retorno = phone.replace(/\D/g, "");
14 | retorno = retorno.replace(/^0/, "");
15 | if (retorno.length > 10) {
16 | retorno = retorno.replace(/^(\d\d)(\d{5})(\d{4}).*/, "($1) $2-$3");
17 | } else if (retorno.length > 5) {
18 | // if (retorno.length == 6 && event.code == "Backspace") {
19 | // // necessário pois senão o "-" fica sempre voltando ao dar backspace
20 | // return;
21 | // }
22 | retorno = retorno.replace(/^(\d\d)(\d{4})(\d{0,4}).*/, "($1) $2-$3");
23 | } else if (retorno.length > 2) {
24 | retorno = retorno.replace(/^(\d\d)(\d{0,5})/, "($1) $2");
25 | } else {
26 | if (retorno.length !== 0) {
27 | retorno = retorno.replace(/^(\d*)/, "($1");
28 | }
29 | }
30 |
31 | return retorno
32 | }
--------------------------------------------------------------------------------
/src/view/CreateNewDeliveryView.tsx:
--------------------------------------------------------------------------------
1 | import React, { FormEvent, useState } from "react";
2 | import { Link, useHistory } from "react-router-dom";
3 | import { toast } from "react-toastify";
4 | import { FiArrowLeft, FiSearch } from "react-icons/fi";
5 |
6 | import { useDeliveries } from "../hooks/useDeliveries";
7 |
8 | import { DeliveryInput } from '../types'
9 |
10 | import { Header } from "../components/Header";
11 | import { SearchCustomerModal } from "../components/Modal/SearchCustomerModal";
12 |
13 | import { Button } from "../shared/Button";
14 | import { Input } from "../shared/Input";
15 |
16 | import { Container, Content } from "../styles/view/create-new-delivery";
17 |
18 | export function CreateNewDelivery() {
19 |
20 | const history = useHistory()
21 | const { createDelivery } = useDeliveries()
22 |
23 | const [isSearchCustomerModalOpen, setIsSearchCustomerModalOpen] = useState(false)
24 |
25 | const initialFormState: DeliveryInput = {
26 | cliente: {
27 | id: 0
28 | },
29 | destinatario: {
30 | nome: '',
31 | logradouro: '',
32 | numero: '',
33 | complemento: '',
34 | bairro: '',
35 | },
36 | taxa: 0
37 | }
38 |
39 | const [form, setForm] = useState(initialFormState)
40 |
41 |
42 | async function handleSubmit(event: FormEvent) {
43 | event.preventDefault()
44 |
45 | try {
46 | await createDelivery(form)
47 |
48 | toast.success(`Entrega emitida com sucesso!`)
49 | history.push('/deliveries')
50 | } catch(error) {
51 | console.log(error)
52 | toast.error('Não foi possível salvar os dados da entrega')
53 | }
54 | }
55 |
56 | function handleInputChange(event: React.ChangeEvent) {
57 | const {name, value } = event.target
58 | let formUpdate = {...form};
59 |
60 | switch (name) {
61 | case 'taxa':
62 | formUpdate.taxa = Number(value)
63 | break;
64 |
65 | case 'logradouro':
66 | formUpdate.destinatario.logradouro = value
67 | break;
68 |
69 | case 'numero':
70 | formUpdate.destinatario.numero = value
71 | break;
72 |
73 | case 'complemento':
74 | formUpdate.destinatario.complemento = value
75 | break;
76 |
77 | case 'bairro':
78 | formUpdate.destinatario.bairro = value
79 | break;
80 |
81 | default:
82 | break;
83 | }
84 | setForm(formUpdate)
85 | }
86 |
87 | function handleCustomerSelected(id: number, name: string) {
88 | setForm({
89 | ...form
90 | , cliente: {
91 | id
92 | },
93 | destinatario: {
94 | ...form.destinatario,
95 | nome: name
96 | }
97 | })
98 | }
99 |
100 | function handleOpenSearchCustomerModal() {
101 | setIsSearchCustomerModalOpen(true)
102 | }
103 |
104 | function handleCloseSearchCustomerModal() {
105 | setIsSearchCustomerModalOpen(false)
106 | }
107 |
108 | function handleCancelRegistration() {
109 | history.push('/deliveries')
110 | }
111 |
112 | return(
113 |
114 |
119 |
120 |
121 | Voltar
122 |
123 |
124 |
125 |
126 | }
135 | />
136 |
145 | Endereço para entrega
146 |
147 |
154 |
161 |
168 |
175 |
176 |
177 |
181 | Solicitar entrega
182 |
183 |
187 | Cancelar
188 |
189 |
190 |
191 |
196 |
197 |
198 | )
199 | }
--------------------------------------------------------------------------------
/src/view/CustomersView.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useHistory } from 'react-router-dom'
3 | import { toast } from 'react-toastify';
4 | import { FiEdit2, FiTrash2, FiPlus } from 'react-icons/fi'
5 |
6 | import { useCustomers } from '../hooks/useCustomers';
7 |
8 | import { formatPhone } from '../utils/format';
9 |
10 | import { Customer } from '../types';
11 |
12 | import { Button } from '../shared/Button';
13 | import { Header } from "../components/Header";
14 | import { DeleteCustomerModal } from '../components/Modal/DeleteCustomerModal';
15 |
16 | import { Container, Content } from "../styles/view/customers";
17 |
18 | export function Customers() {
19 | const { customers, deleteCustomer } = useCustomers()
20 |
21 | const customersFormated = customers.map(customer => ({
22 | ...customer,
23 | telefone: formatPhone(customer.telefone)
24 | }))
25 |
26 | const history = useHistory()
27 |
28 | const customersAmount = customers.length
29 | const [customerToDelete, setCustomerToDelete] = useState({} as Customer)
30 |
31 | const [isDeleteCustomerModalOpen, setIsDeleteCustomerModalOpen] = useState(false)
32 |
33 | function handleOpenDeleteCustomerModal(customerToDelete: Customer) {
34 | setIsDeleteCustomerModalOpen(true)
35 | setCustomerToDelete(customerToDelete)
36 | }
37 |
38 | function handleCloseDeleteCustomerModal() {
39 | setIsDeleteCustomerModalOpen(false)
40 | setCustomerToDelete({} as Customer)
41 | }
42 |
43 | function handleDeleteCustomer(customer: Customer) {
44 | deleteCustomer(customer.id)
45 | .then(response => {
46 | toast.success('Cliente excluído')
47 | handleCloseDeleteCustomerModal()
48 | })
49 | .catch(response => {
50 | console.log(response)
51 | toast.error('Existe entregas para esse cliente. Não pode ser excluído')
52 | })
53 | }
54 |
55 | function goToCreateNewCustomer() {
56 | history.push('customers/new')
57 | }
58 |
59 | function goToEditCustomer(customerId: number) {
60 | history.push(`/customers/${customerId}`)
61 | }
62 |
63 | return(
64 |
65 | 1 ? 'clientes cadastrados' : 'cliente cadastrado'}`}
69 | >
70 |
75 |
76 | Novo cliente
77 |
78 |
79 |
80 |
81 |
82 |
83 | Nome
84 | E-mail
85 | Telefone
86 |
87 |
88 |
89 |
90 | {customersFormated.map(customer => (
91 |
92 | { customer.nome }
93 | { customer.email }
94 | { customer.telefone }
95 |
96 | goToEditCustomer(customer.id)}
99 | >
100 |
101 |
102 |
103 | handleOpenDeleteCustomerModal(customer)}
106 | >
107 |
108 |
109 |
110 |
111 | ))}
112 |
113 |
114 |
115 | handleDeleteCustomer(customerToDelete)}
119 | customerToDelete={customerToDelete}
120 | />
121 |
122 |
123 |
124 | );
125 | }
--------------------------------------------------------------------------------
/src/view/DashboardView.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { FiChevronRight } from 'react-icons/fi'
3 |
4 | import { useDeliveries } from '../hooks/useDeliveries';
5 | import { useCustomers } from '../hooks/useCustomers';
6 |
7 | import { Header } from '../components/Header';
8 | import { Icon } from '../components/Icon';
9 |
10 | import { Container, Content } from '../styles/view/dashboard'
11 |
12 | export function Dashboard() {
13 |
14 | const { deliveries } = useDeliveries()
15 | const { customers } = useCustomers()
16 |
17 | const deliveriesAmount = deliveries.length
18 | const customersAmount = customers.length
19 |
20 |
21 | return (
22 |
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Entregas
36 | {deliveriesAmount}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
Clientes
48 | {customersAmount}
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
--------------------------------------------------------------------------------
/src/view/DeliveriesView.tsx:
--------------------------------------------------------------------------------
1 | import { Link, useHistory } from 'react-router-dom'
2 | import { FiPlus } from 'react-icons/fi';
3 |
4 | import { formatDateTime, formatPrice } from '../utils/format';
5 |
6 | import { useDeliveries } from '../hooks/useDeliveries';
7 |
8 | import { Header } from "../components/Header";
9 | import { Button } from '../shared/Button';
10 |
11 | import { Container, Content, DeliveriesContainer } from "../styles/view/deliveries";
12 |
13 | export function Deliveries() {
14 | const history = useHistory()
15 |
16 | const { deliveries } = useDeliveries()
17 |
18 | const deliveriesAmount = deliveries.length
19 | const deliveriesSorted = deliveries.sort((a, b) => a.dataPedido > b.dataPedido ? -1 : 1)
20 |
21 | function handleLinkToCreateNewDelivery() {
22 | history.push('deliveries/new')
23 | }
24 |
25 | return(
26 |
27 | 1 ? 'solicitações' : 'solicitação'}`}
31 | >
32 |
37 |
38 | Nova entrega
39 |
40 |
41 |
42 |
43 | {deliveriesSorted.map(delivery => (
44 |
45 |
46 |
47 |
48 | Data do pedido
49 | {formatDateTime(new Date(delivery.dataPedido))}
50 |
51 |
{delivery.status}
52 |
53 |
54 |
55 |
Cliente
56 |
{delivery.cliente.nome}
57 |
58 |
59 | Taxa de entrega
60 | {formatPrice(delivery.taxa)}
61 |
62 |
63 |
64 |
65 | ))}
66 |
67 |
68 |
69 | )
70 | }
--------------------------------------------------------------------------------
/src/view/DeliveryDetailsView.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, useEffect, useState } from "react";
2 | import { Link, useParams } from "react-router-dom";
3 | import { toast } from "react-toastify";
4 | import { FaArrowLeft, FaPlus } from "react-icons/fa";
5 |
6 | import { useDeliveries } from "../hooks/useDeliveries";
7 |
8 | import { api } from "../services/api";
9 |
10 | import { formatDateTime, formatPrice } from "../utils/format";
11 |
12 | import { Delivery, Occurrence } from "../types";
13 |
14 | import { Header } from "../components/Header";
15 | import { NewOccurrenceModal } from "../components/Modal/NewOccurrenceModal";
16 | import { Button } from "../shared/Button";
17 |
18 | import { Container, Content } from "../styles/view/delivery-details";
19 |
20 | interface DetalhesEntregaParams {
21 | id: string
22 | }
23 |
24 | interface DeliveryFormatted {
25 | id: number,
26 | cliente: {
27 | id: number,
28 | nome: string
29 | },
30 | destinatario: {
31 | nome: string,
32 | logradouro: string,
33 | numero: string,
34 | complemento: string,
35 | bairro: string
36 | },
37 | taxa: number,
38 | taxaFormatada: string,
39 | status: 'FINALIZADA' | 'PENDENTE' | 'CANCELADA',
40 | dataPedido: string,
41 | dataFinalizacao: string
42 | }
43 |
44 | export function DeliveryDetails() {
45 | const params = useParams()
46 | const id = Number(params.id)
47 |
48 | const { changeStatus } = useDeliveries()
49 |
50 | const [deliveryFormatted, setDeliveryFormatted] = useState()
51 | const [occurrences, setOccurrences] = useState([])
52 | const [statusChanged, setStatusChanged] = useState('')
53 |
54 | useEffect(() => {
55 | api.get(`/entregas/${id}`)
56 | .then(response => {
57 | const delivery = response.data
58 |
59 | setDeliveryFormatted({
60 | ...delivery,
61 | taxaFormatada: formatPrice(delivery.taxa),
62 | dataPedido: formatDateTime(new Date(delivery.dataPedido)),
63 | dataFinalizacao: delivery.dataFinalizacao !== null ? formatDateTime(new Date(delivery.dataFinalizacao)) : '-'
64 | })
65 | })
66 | }, [id, statusChanged])
67 |
68 | useEffect(() => {
69 | loadOcorrencias()
70 | // eslint-disable-next-line react-hooks/exhaustive-deps
71 | }, [])
72 |
73 | async function handleChangeStatusDelivery(event: ChangeEvent) {
74 | const status = event.target.value
75 |
76 | await changeStatus(id, status.toUpperCase())
77 |
78 | toast.success(`Entrega Nº ${id} ${status.toLowerCase()}`)
79 |
80 | setStatusChanged(status)
81 | }
82 |
83 | function loadOcorrencias() {
84 | api.get(`/entregas/${id}/ocorrencias`)
85 | .then(response => {
86 | const occurrences = response.data.map(occurrence => ({
87 | ...occurrence,
88 | dataRegistro: formatDateTime(new Date(occurrence.dataRegistro))
89 | }))
90 | setOccurrences(occurrences)
91 | })
92 | }
93 |
94 | const [isNewOccurrenceModalOpen, setIsNewOccurrenceModalOpen] = useState(false)
95 |
96 | function handleOpenNewOccurrenceModal() {
97 | setIsNewOccurrenceModalOpen(true)
98 | }
99 |
100 | function handleCloseNewOccurrenceModal() {
101 | setIsNewOccurrenceModalOpen(false)
102 | }
103 |
104 | return(
105 |
106 |
111 |
112 |
113 | Voltar
114 |
115 |
116 |
117 |
118 |
119 | Data do pedido
120 | {deliveryFormatted?.dataPedido}
121 |
122 |
123 | Taxa de entrega
124 | {deliveryFormatted?.taxaFormatada}
125 |
126 |
127 | Data finalização
128 | {deliveryFormatted?.dataFinalizacao}
129 |
130 |
131 | Status
132 |
139 | Pendente
140 | Finalizada
141 | Cancelada
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
Cliente
150 |
151 |
152 |
153 |
Nome
154 |
{deliveryFormatted?.cliente.nome}
155 |
156 | {/*
157 |
158 |
*/}
159 |
160 |
161 |
Endereço
162 |
163 |
164 |
Nome
165 |
{deliveryFormatted?.cliente.nome}
166 |
167 |
168 |
Logradouro
169 |
{deliveryFormatted?.destinatario.logradouro}
170 |
171 |
172 |
173 |
Número
174 |
{deliveryFormatted?.destinatario.numero}
175 |
176 |
177 |
Complemento
178 |
{deliveryFormatted?.destinatario.complemento}
179 |
180 |
181 |
Bairro
182 |
{deliveryFormatted?.destinatario.bairro}
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
Ocorrências
192 |
193 | {`${occurrences.length} ${occurrences.length > 1 ? 'ocorrências' : 'ocorrência'}`}
194 |
195 |
196 |
197 |
203 |
204 | Nova ocorrência
205 |
206 |
207 |
208 | {occurrences.map(occurrence => (
209 |
210 | {occurrence.descricao}
211 | {occurrence.dataRegistro}
212 |
213 |
214 | ))}
215 |
216 | {deliveryFormatted?.status === 'FINALIZADA' ? (
217 |
218 | Entrega finalizada
219 | {deliveryFormatted?.dataFinalizacao}
220 |
221 |
222 | ) : null}
223 |
224 |
225 |
226 |
227 |
233 |
234 |
235 | )
236 | }
--------------------------------------------------------------------------------
/src/view/RegistrationCustomerView.tsx:
--------------------------------------------------------------------------------
1 | import React, { FormEvent, useEffect, useState } from "react";
2 | import { Link, useHistory, useParams } from "react-router-dom";
3 | import { toast } from "react-toastify";
4 | import { FaArrowLeft } from 'react-icons/fa'
5 |
6 | import { useCustomers } from "../hooks/useCustomers";
7 |
8 | import { Customer } from "../types";
9 |
10 | import { Header } from "../components/Header";
11 | import { Button } from "../shared/Button";
12 | import { Input } from "../shared/Input";
13 |
14 | import { Container, Content } from "../styles/view/registration-customer";
15 |
16 | interface InitialFormState {
17 | id?: number;
18 | nome: string;
19 | email: string;
20 | telefone: string;
21 | }
22 |
23 | interface CadastroClienteParams {
24 | id?: string;
25 | }
26 |
27 | export function RegistrationCustomer() {
28 |
29 | const { createCustomer, updateCustomer, showCustomer } = useCustomers()
30 |
31 | const history = useHistory()
32 |
33 | const { id } = useParams()
34 | const isUpdate = Number(id) ? true : false
35 |
36 | const [form, setForm] = useState({
37 | nome: '',
38 | email: '',
39 | telefone: ''
40 | })
41 |
42 | useEffect(() => {
43 | if(isUpdate) {
44 | showCustomer(Number(id))
45 | .then(customer => {
46 | const customerFormated = {
47 | ...customer,
48 | telefone: customer.telefone.replace(/D/g,"")
49 | }
50 |
51 | setForm(customerFormated)
52 | })
53 | }
54 |
55 | // eslint-disable-next-line react-hooks/exhaustive-deps
56 | }, [id, isUpdate])
57 |
58 | function handleInputChange(event: React.ChangeEvent) {
59 | const {name, value} = event.target
60 |
61 | setForm({
62 | ...form,
63 | [name]: value
64 | })
65 | }
66 |
67 | function handleFormSubmit(event: FormEvent) {
68 | event.preventDefault()
69 |
70 | form.id
71 | ? onUpdateCustomer(form as Customer)
72 | : onCreateNewCustomer(form)
73 | }
74 |
75 | async function onCreateNewCustomer(customer: InitialFormState) {
76 | try {
77 | const newCustomer = {...customer, telefone: handleFormatPhone()}
78 |
79 | await createCustomer(newCustomer)
80 |
81 | toast.success(`Cliente cadastrado com sucesso`)
82 | history.push('/customers')
83 | } catch(error) {
84 | console.log(error)
85 | toast.error('Não foi possível salvar os dados do cliente')
86 | }
87 | }
88 |
89 | async function onUpdateCustomer(customer: Customer) {
90 | try {
91 | const newCustomer = {...customer, telefone: handleFormatPhone()}
92 |
93 | await updateCustomer(newCustomer)
94 |
95 | toast.success('Dados do cliente atualizados')
96 | history.push('/customers')
97 | } catch(error) {
98 | console.log(error)
99 | toast.error('Dados do cliente não atualizados')
100 | }
101 | }
102 |
103 | function handleCancelRegistration() {
104 | history.push('/customers')
105 | }
106 |
107 | function handleFormatPhone() {
108 | const phone = form.telefone
109 | .replace(/D/g, "")
110 |
111 | return phone
112 | }
113 |
114 | const labelOfButtonSubmit = isUpdate ? 'Atualizar' : 'Salvar'
115 |
116 | return(
117 |
118 |
123 |
124 |
125 | Voltar
126 |
127 |
128 |
129 |
130 |
137 |
138 |
145 |
154 |
155 |
156 |
160 | {labelOfButtonSubmit}
161 |
162 |
166 | Cancelar
167 |
168 |
169 |
170 |
171 |
172 | )
173 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------