├── .editorconfig
├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── package.json
├── prettier.config.js
├── public
├── _redirects
├── favicon.svg
└── index.html
├── src
├── @types
│ └── styled.d.ts
├── App.tsx
├── assets
│ ├── authenticate-background.svg
│ ├── background.svg
│ ├── dashboard-background.svg
│ ├── favicon.svg
│ ├── footer-background.svg
│ ├── high-background.svg
│ ├── images
│ │ ├── high-background.png
│ │ ├── profile.png
│ │ └── project-example.png
│ ├── logo.svg
│ ├── not-found.svg
│ ├── profile-background.svg
│ └── profile.svg
├── components
│ ├── Alert
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── AnswerModal
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Button
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Input
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── Shimmer
│ │ └── index.ts
│ ├── TextArea
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── ToastContainer
│ │ ├── Toast
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── index.tsx
│ │ └── styles.ts
│ └── Tooltip
│ │ ├── index.tsx
│ │ └── styles.ts
├── config
│ └── chart.ts
├── hooks
│ ├── auth.tsx
│ ├── index.tsx
│ ├── theme.tsx
│ └── toast.tsx
├── index.tsx
├── modules
│ ├── dashboard
│ │ ├── components
│ │ │ ├── Card
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.ts
│ │ │ ├── Header
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.ts
│ │ │ └── Sidebar
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.ts
│ │ └── screens
│ │ │ ├── Authenticate
│ │ │ └── index.tsx
│ │ │ ├── Main
│ │ │ ├── index.tsx
│ │ │ ├── sections
│ │ │ │ └── LineChart
│ │ │ │ │ └── index.tsx
│ │ │ └── styles.ts
│ │ │ ├── Projects
│ │ │ ├── FileList
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.ts
│ │ │ ├── Skeleton
│ │ │ │ └── index.tsx
│ │ │ ├── Upload
│ │ │ │ ├── index.tsx
│ │ │ │ └── styles.ts
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ │ ├── Tenders
│ │ │ ├── Skeleton
│ │ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ │ └── _layouts
│ │ │ ├── auth
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ │ └── dashboard
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ └── users
│ │ ├── components
│ │ ├── Footer
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── Header
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── screens
│ │ └── Main
│ │ ├── index.tsx
│ │ ├── sections
│ │ ├── CreateProject
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Presentation
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Profile
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── Projects
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── styles.ts
├── react-app-env.d.ts
├── routes
│ ├── Route.tsx
│ └── index.tsx
├── services
│ └── api.ts
├── setupTests.ts
├── styles
│ ├── global.ts
│ └── themes
│ │ ├── dark.ts
│ │ └── light.ts
└── utils
│ ├── getClickOutside.tsx
│ └── getValidationErrors.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 | REACT_APP_API_URL=http://localhost:3333
3 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.js
2 | node_modules
3 | build
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true
6 | },
7 | "extends": [
8 | "plugin:react/recommended",
9 | "airbnb",
10 | "plugin:@typescript-eslint/recommended",
11 | "prettier/@typescript-eslint",
12 | "plugin:prettier/recommended"
13 | ],
14 | "globals": {
15 | "Atomics": "readonly",
16 | "SharedArrayBuffer": "readonly"
17 | },
18 | "parser": "@typescript-eslint/parser",
19 | "parserOptions": {
20 | "ecmaFeatures": {
21 | "jsx": true
22 | },
23 | "ecmaVersion": 2018,
24 | "sourceType": "module"
25 | },
26 | "plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
27 | "rules": {
28 | "prettier/prettier": "error",
29 | "react/jsx-one-expression-per-line": "off",
30 | "react/jsx-props-no-spreading": "off",
31 | "react/prop-types": "off",
32 | "react-hooks/rules-of-hooks": "error",
33 | "react-hooks/exhaustive-deps": "warn",
34 | "no-unused-expressions": "off",
35 | "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }],
36 | "import/prefer-default-export": "off",
37 | "import/no-duplicates": "off",
38 | "no-nested-ternary": "off",
39 | "camelcase": "off",
40 | "react/jsx-wrap-multilines": "off",
41 | "react/jsx-curly-newline": "off",
42 | "@typescript-eslint/ban-types": "off",
43 | "@typescript-eslint/explicit-function-return-type": [
44 | "error",
45 | {
46 | "allowExpressions": true
47 | }
48 | ],
49 | "import/extensions": [
50 | "error",
51 | "ignorePackages",
52 | {
53 | "ts": "never",
54 | "tsx": "never"
55 | }
56 | ]
57 | },
58 | "settings": {
59 | "import/resolver": {
60 | "typescript": {}
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/.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 | .env
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "@types/jest": "^24.0.0",
10 | "@types/node": "^12.0.0",
11 | "@types/react": "^16.9.0",
12 | "@types/react-dom": "^16.9.0",
13 | "@types/styled-components": "^5.1.0",
14 | "@unform/core": "^2.1.3",
15 | "@unform/web": "^2.1.3",
16 | "axios": "^0.19.2",
17 | "chart.js": "^2.9.3",
18 | "date-fns": "^2.14.0",
19 | "filesize": "^6.1.0",
20 | "polished": "^3.6.5",
21 | "react": "^16.13.1",
22 | "react-chartjs-2": "^2.9.0",
23 | "react-charts": "^2.0.0-beta.7",
24 | "react-day-picker": "^7.4.8",
25 | "react-dom": "^16.13.1",
26 | "react-dropzone": "^11.0.1",
27 | "react-icons": "^3.10.0",
28 | "react-router-dom": "^5.2.0",
29 | "react-scripts": "3.4.1",
30 | "react-scroll": "^1.7.16",
31 | "react-select": "^3.1.0",
32 | "react-spring": "^8.0.27",
33 | "react-switch": "^5.0.1",
34 | "styled-components": "^5.1.1",
35 | "typescript": "~3.7.2",
36 | "uuidv4": "^6.1.1",
37 | "yup": "^0.29.1"
38 | },
39 | "scripts": {
40 | "start": "react-scripts start",
41 | "build": "react-scripts build",
42 | "test": "react-scripts test",
43 | "eject": "react-scripts eject"
44 | },
45 | "eslintConfig": {
46 | "extends": "react-app"
47 | },
48 | "browserslist": {
49 | "production": [
50 | ">0.2%",
51 | "not dead",
52 | "not op_mini all"
53 | ],
54 | "development": [
55 | "last 1 chrome version",
56 | "last 1 firefox version",
57 | "last 1 safari version"
58 | ]
59 | },
60 | "devDependencies": {
61 | "@types/chart.js": "^2.9.22",
62 | "@types/react-dropzone": "^5.1.0",
63 | "@types/react-router-dom": "^5.1.5",
64 | "@types/react-scroll": "^1.5.5",
65 | "@types/uuidv4": "^5.0.0",
66 | "@types/yup": "^0.29.3",
67 | "@typescript-eslint/eslint-plugin": "^3.5.0",
68 | "@typescript-eslint/parser": "^3.5.0",
69 | "eslint": "^7.4.0",
70 | "eslint-config-airbnb": "^18.2.0",
71 | "eslint-config-prettier": "^6.11.0",
72 | "eslint-import-resolver-typescript": "^2.0.0",
73 | "eslint-plugin-import": "2.21.2",
74 | "eslint-plugin-jsx-a11y": "6.3.0",
75 | "eslint-plugin-prettier": "^3.1.4",
76 | "eslint-plugin-react": "7.20.0",
77 | "eslint-plugin-react-hooks": "4",
78 | "prettier": "^2.0.5"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | trailingComma: "all",
4 | arrowParens: "avoid"
5 | };
6 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 | /dashboard /dashboard/statistics 200
3 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 | Gabriel Teodoro
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/@types/styled.d.ts:
--------------------------------------------------------------------------------
1 | import 'styled-components';
2 |
3 | declare module 'styled-components' {
4 | export interface DefaultTheme {
5 | title: string;
6 |
7 | colors: {
8 | primary: string;
9 | error: string;
10 | warning: string;
11 | success: string;
12 |
13 | background: string;
14 | backgroundSecundary: string;
15 |
16 | text: string;
17 | muted: string;
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import GlobalStyle from './styles/global';
4 |
5 | import Routes from './routes';
6 |
7 | import AppProvider from './hooks';
8 |
9 | const App: React.FC = () => (
10 | <>
11 |
12 |
13 |
14 |
15 | >
16 | );
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/assets/authenticate-background.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/dashboard-background.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/footer-background.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/images/high-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oigabrielteodoro/react-deploy/58a7839a846c4fcc964cecb11d5e1be74b732710/src/assets/images/high-background.png
--------------------------------------------------------------------------------
/src/assets/images/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oigabrielteodoro/react-deploy/58a7839a846c4fcc964cecb11d5e1be74b732710/src/assets/images/profile.png
--------------------------------------------------------------------------------
/src/assets/images/project-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oigabrielteodoro/react-deploy/58a7839a846c4fcc964cecb11d5e1be74b732710/src/assets/images/project-example.png
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/not-found.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/assets/profile-background.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/components/Alert/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container } from './styles';
4 |
5 | interface IAlertProps {
6 | type: 'error' | 'success' | 'info';
7 | isVisible: boolean;
8 | }
9 |
10 | const Alert: React.FC = ({ children, type, isVisible }) => {
11 | return (
12 |
13 | {children}
14 |
15 | );
16 | };
17 |
18 | export default Alert;
19 |
--------------------------------------------------------------------------------
/src/components/Alert/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | interface IContainerProps {
4 | type: 'error' | 'success' | 'info';
5 | isVisible: boolean;
6 | }
7 |
8 | const alertTypeVariations = {
9 | info: css`
10 | background: #cbc5ea;
11 | color: #7051dc;
12 | `,
13 | success: css`
14 | background: #e6fffa;
15 | color: #2e656a;
16 | `,
17 | error: css`
18 | background: #fddede;
19 | color: #c53030;
20 | `,
21 | };
22 |
23 | export const Container = styled.div`
24 | width: 100%;
25 | border-radius: 4px;
26 | padding: 15px 25px;
27 | text-align: center;
28 |
29 | display: ${({ isVisible }) => (isVisible ? 'flex' : 'none')};
30 |
31 | ${({ type }) => alertTypeVariations[type || 'info']};
32 | `;
33 |
--------------------------------------------------------------------------------
/src/components/AnswerModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useCallback, MouseEvent } from 'react';
2 | import { MdClose } from 'react-icons/md';
3 |
4 | import { Overlay, Container, CloseButton } from './styles';
5 |
6 | interface IAnswerModalProps {
7 | visible: boolean;
8 | onCancel(): void;
9 | }
10 |
11 | const AnswerModal: React.FC = ({
12 | visible,
13 | children,
14 | onCancel,
15 | }) => {
16 | const ref = useRef(null);
17 |
18 | const handleOverlayClick = useCallback(
19 | (event: MouseEvent) => {
20 | if (event.target === ref.current) {
21 | onCancel();
22 | }
23 | },
24 | [onCancel, ref],
25 | );
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 | );
37 | };
38 |
39 | export default AnswerModal;
40 |
--------------------------------------------------------------------------------
/src/components/AnswerModal/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | interface IOverlayProps {
4 | visible: boolean;
5 | }
6 |
7 | export const Overlay = styled.div`
8 | position: fixed;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | width: 100%;
13 | height: 100%;
14 | top: 0;
15 | left: 0;
16 | padding: 20px;
17 | background: #00000090;
18 | color: #fff;
19 | opacity: 0;
20 | visibility: hidden;
21 | transition: 0.2s ease-in 0.2s;
22 | cursor: pointer;
23 | z-index: 9999;
24 |
25 | ${({ visible }) =>
26 | visible &&
27 | css`
28 | opacity: 1;
29 | visibility: visible;
30 | transition: 0.3s;
31 |
32 | > div {
33 | opacity: 1;
34 | transform: translateY(0);
35 | transition: 0.3s ease-out 0.2s;
36 | }
37 | `};
38 | `;
39 |
40 | export const Container = styled.div`
41 | position: relative;
42 | width: 100%;
43 | max-width: 700px;
44 | max-height: 90vh;
45 | padding: 40px 25px;
46 | background: #202024;
47 | box-shadow: 0 5px 30px #00000090;
48 | opacity: 0;
49 | border-radius: 5px;
50 | transform: translateY(20px);
51 | transition: 0.2s ease-in;
52 | overflow-y: auto;
53 | text-align: left;
54 | cursor: default;
55 | overflow: hidden;
56 |
57 | h4 {
58 | font-size: 24px;
59 | font-weight: bold;
60 | text-align: center;
61 | }
62 |
63 | > a {
64 | display: block;
65 | margin-top: 44px;
66 | text-align: center;
67 | font-size: 14px;
68 | font-weight: bold;
69 | color: #7159c1;
70 | transition: color 0.2s;
71 |
72 | &:hover {
73 | color: #7c62d4;
74 | }
75 | }
76 |
77 | > footer {
78 | display: flex;
79 | flex-direction: column;
80 | align-items: center;
81 | justify-content: center;
82 |
83 | p {
84 | font-size: 12px;
85 | text-align: center;
86 | color: #87868b;
87 | }
88 | }
89 |
90 | @media (max-width: 580px) {
91 | padding: 50px 24px 40px;
92 |
93 | h4 {
94 | font-size: 20px;
95 | }
96 | }
97 | `;
98 |
99 | export const CloseButton = styled.button`
100 | position: absolute;
101 | width: 40px;
102 | height: 40px;
103 | top: 10px;
104 | right: 10px;
105 | display: flex;
106 | align-items: center;
107 | justify-content: center;
108 | background: transparent;
109 | color: #87868b;
110 | border: 0;
111 | border-radius: 5px;
112 | cursor: pointer;
113 | transition: 0.3s;
114 |
115 | svg {
116 | width: 24px;
117 | height: 24px;
118 | }
119 |
120 | &:hover {
121 | background: #28272e;
122 | color: #fff;
123 | }
124 |
125 | &:disabled {
126 | opacity: 0.5;
127 | }
128 | `;
129 |
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ButtonHTMLAttributes } from 'react';
2 |
3 | import { Container } from './styles';
4 |
5 | type ButtonProps = ButtonHTMLAttributes;
6 |
7 | const Button: React.FC = ({ children, ...rest }) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | };
14 |
15 | export default Button;
16 |
--------------------------------------------------------------------------------
/src/components/Button/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { shade } from 'polished';
3 |
4 | export const Container = styled.button`
5 | background: #7051dc;
6 | height: 60px;
7 | border-radius: 4px;
8 | border: 0;
9 | padding: 0 16px;
10 | color: #f4ede8;
11 | width: 100%;
12 | font-weight: 500;
13 | margin-top: 16px;
14 | transition: all 0.2s;
15 |
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 |
20 | &:disabled {
21 | cursor: no-drop;
22 | }
23 |
24 | &:hover {
25 | background: ${shade(0.2, '#7051DC')};
26 | }
27 | `;
28 |
--------------------------------------------------------------------------------
/src/components/Input/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | InputHTMLAttributes,
3 | useEffect,
4 | useRef,
5 | useState,
6 | useCallback,
7 | useMemo,
8 | CSSProperties,
9 | } from 'react';
10 | import { IconBaseProps } from 'react-icons';
11 | import { FiAlertCircle } from 'react-icons/fi';
12 | import { useField } from '@unform/core';
13 |
14 | import { Container, Error } from './styles';
15 |
16 | interface InputProps extends InputHTMLAttributes {
17 | name: string;
18 | icon?: React.ComponentType;
19 | containerStyle?: CSSProperties;
20 | }
21 |
22 | const Input: React.FC = ({
23 | name,
24 | icon: Icon,
25 | type,
26 | containerStyle,
27 | ...rest
28 | }) => {
29 | const inputRef = useRef(null);
30 |
31 | const [isFocused, setIsFocused] = useState(false);
32 | const [isFilled, setIsFilled] = useState(false);
33 |
34 | const { fieldName, defaultValue, error, registerField } = useField(name);
35 |
36 | const handleInputFocus = useCallback(() => {
37 | setIsFocused(true);
38 | }, []);
39 |
40 | const handleInputBlur = useCallback(() => {
41 | setIsFocused(false);
42 |
43 | setIsFilled(!!inputRef.current?.value);
44 | }, []);
45 |
46 | useEffect(() => {
47 | registerField({
48 | name: fieldName,
49 | ref: inputRef.current,
50 | path: 'value',
51 | });
52 | }, [fieldName, registerField]);
53 |
54 | const isRadio = useMemo(() => {
55 | return type === 'radio';
56 | }, [type]);
57 |
58 | return (
59 |
66 | {Icon && }
67 |
68 |
76 |
77 | {error && (
78 |
79 |
80 |
81 | )}
82 |
83 | );
84 | };
85 |
86 | export default Input;
87 |
--------------------------------------------------------------------------------
/src/components/Input/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | import Tooltip from '../Tooltip';
4 |
5 | interface ContainerProps {
6 | isFocused: boolean;
7 | isFilled: boolean;
8 | isErrored: boolean;
9 | isRadio: boolean;
10 | }
11 |
12 | export const Container = styled.div`
13 | background: ${({ isRadio, theme }) =>
14 | isRadio ? 'transparent' : theme.colors.background};
15 | border-radius: 4px;
16 | border: 2px solid ${({ isRadio, theme }) =>
17 | isRadio ? 'transparent' : theme.colors.background};
18 | color: #A8A8B3;
19 | display: flex;
20 | align-items: center;
21 |
22 | ${({ isRadio }) =>
23 | !isRadio &&
24 | css`
25 | padding: 16px;
26 | width: 100%;
27 | `}
28 |
29 | & + div {
30 | margin-top: 30px;
31 | }
32 |
33 | ${({ isErrored, isRadio }) =>
34 | isErrored &&
35 | css`
36 | border-color: ${isRadio ? 'transparent' : '#c53030'};
37 | `}
38 |
39 | ${({ isFocused, isRadio }) =>
40 | isFocused &&
41 | css`
42 | color: #7051dc;
43 | border-color: ${isRadio ? 'transparent' : '#7051dc'};
44 | `}
45 |
46 | ${({ isFilled }) =>
47 | isFilled &&
48 | css`
49 | color: #7051dc;
50 | `}
51 |
52 | input {
53 | flex: 1;
54 | background: transparent;
55 | border: 0;
56 | color: #f4ede8;
57 | opacity: ${({ isRadio }) => (isRadio ? 0 : 1)};
58 |
59 | &:disabled {
60 | cursor: no-drop;
61 | }
62 |
63 | &::placeholder {
64 | color: #A8A8B3;
65 | }
66 | }
67 |
68 | svg {
69 | margin-right: 16px;
70 | }
71 | `;
72 |
73 | export const Error = styled(Tooltip)`
74 | margin-left: 16px;
75 |
76 | svg {
77 | margin: 0;
78 | }
79 |
80 | span {
81 | background: #c53030;
82 | color: #fff;
83 |
84 | &::before {
85 | border-color: #c53030 transparent;
86 | }
87 | }
88 | `;
89 |
--------------------------------------------------------------------------------
/src/components/Shimmer/index.ts:
--------------------------------------------------------------------------------
1 | import { darken, lighten } from 'polished';
2 | import styled, { keyframes } from 'styled-components';
3 |
4 | interface IShimmerEffectProps {
5 | type?: 'title' | 'image' | 'body';
6 | width?: number;
7 | height?: number;
8 | }
9 |
10 | const progress = keyframes`
11 | 0% {
12 | background-position: -200px 0;
13 | }
14 | 100% {
15 | background-position: calc(200px + 100%) 0;
16 | }
17 | `;
18 |
19 | const ShimmerEffect = styled.span`
20 | animation: ${progress} 1.2s ease-in-out infinite;
21 | height: 13px;
22 | background-image: linear-gradient(
23 | 90deg,
24 | rgba(0, 0, 0, 0),
25 | ${lighten(1, '#fff')},
26 | rgba(0, 0, 0, 0)
27 | );
28 | background-color: ${darken(0.3, '#fff')};
29 | background-size: 200px 100%;
30 | background-repeat: no-repeat;
31 | border-radius: 3px;
32 | display: inline-block;
33 | line-height: 1;
34 | width: 100%;
35 | opacity: 0.1;
36 |
37 | ${({ type }) =>
38 | type === 'image' &&
39 | `
40 | height: 52px;
41 | width: 52px;
42 | border-radius: 150px;
43 | margin-right: 2.4rem;
44 | `}
45 |
46 | ${({ type }) =>
47 | type === 'title' &&
48 | `
49 | height: 25px;
50 | width: 86%;
51 | margin: 20px 0;
52 | @media (max-width: 768px) {
53 | width: 50%;
54 | }
55 | `}
56 |
57 | ${({ type }) =>
58 | type === 'body' &&
59 | `
60 | margin: 3px 0;
61 | `}
62 |
63 | ${({ width }) => width && `width: ${width}px;`}
64 | ${({ height }) => height && `height: ${height}px;`}
65 | `;
66 |
67 | export default ShimmerEffect;
68 |
--------------------------------------------------------------------------------
/src/components/TextArea/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | InputHTMLAttributes,
3 | useEffect,
4 | useRef,
5 | useState,
6 | useCallback,
7 | } from 'react';
8 | import { IconBaseProps } from 'react-icons';
9 | import { FiAlertCircle } from 'react-icons/fi';
10 | import { useField } from '@unform/core';
11 |
12 | import { Container, Error } from './styles';
13 |
14 | interface TextAreaProps extends InputHTMLAttributes {
15 | name: string;
16 | icon?: React.ComponentType;
17 | }
18 |
19 | const TextArea: React.FC = ({ name, icon: Icon, ...rest }) => {
20 | const textAreaRef = useRef(null);
21 |
22 | const [isFocused, setIsFocused] = useState(false);
23 | const [isFilled, setIsFilled] = useState(false);
24 |
25 | const { fieldName, defaultValue, error, registerField } = useField(name);
26 |
27 | const handleInputFocus = useCallback(() => {
28 | setIsFocused(true);
29 | }, []);
30 |
31 | const handleInputBlur = useCallback(() => {
32 | setIsFocused(false);
33 |
34 | setIsFilled(!!textAreaRef.current?.value);
35 | }, []);
36 |
37 | useEffect(() => {
38 | registerField({
39 | name: fieldName,
40 | ref: textAreaRef.current,
41 | path: 'value',
42 | });
43 | }, [fieldName, registerField]);
44 |
45 | return (
46 |
47 | {Icon && }
48 |
49 |
56 |
57 | {error && (
58 |
59 |
60 |
61 | )}
62 |
63 | );
64 | };
65 |
66 | export default TextArea;
67 |
--------------------------------------------------------------------------------
/src/components/TextArea/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | import Tooltip from '../Tooltip';
4 |
5 | interface ContainerProps {
6 | isFocused: boolean;
7 | isFilled: boolean;
8 | isErrored: boolean;
9 | }
10 |
11 | export const Container = styled.div`
12 | background: #08081A;
13 | border-radius: 4px;
14 | border: 2px solid #08081A;
15 | color: #A8A8B3;
16 | display: flex;
17 | padding: 16px;
18 | width: 100%;
19 | min-height: 200px;
20 |
21 | & + div {
22 | margin-top: 30px;
23 | }
24 |
25 | ${({ isErrored }) =>
26 | isErrored &&
27 | css`
28 | border-color: #c53030;
29 | `}
30 |
31 | ${({ isFocused }) =>
32 | isFocused &&
33 | css`
34 | color: #7051dc;
35 | border-color: #7051dc;
36 | `}
37 |
38 | ${({ isFilled }) =>
39 | isFilled &&
40 | css`
41 | color: #7051dc;
42 | `}
43 |
44 | textarea {
45 | flex: 1;
46 | background: transparent;
47 | border: 0;
48 | color: #f4ede8;
49 | resize: none;
50 | font-size: 16px;
51 |
52 | &::placeholder {
53 | color: #A8A8B3;
54 | }
55 | }
56 |
57 | svg {
58 | margin-right: 16px;
59 | }
60 | `;
61 |
62 | export const Error = styled(Tooltip)`
63 | height: 20px;
64 | margin-left: 16px;
65 |
66 | svg {
67 | margin: 0;
68 | }
69 |
70 | span {
71 | background: #c53030;
72 | color: #fff;
73 | &::before {
74 | border-color: #c53030 transparent;
75 | }
76 | }
77 | `;
78 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/Toast/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import {
3 | FiAlertCircle,
4 | FiCheckCircle,
5 | FiInfo,
6 | FiXCircle,
7 | } from 'react-icons/fi';
8 |
9 | import { ToastMessage, useToast } from '../../../hooks/toast';
10 |
11 | import { Container } from './styles';
12 |
13 | interface ToastProps {
14 | toast: ToastMessage;
15 | style: object;
16 | }
17 |
18 | const icons = {
19 | info: ,
20 | error: ,
21 | success: ,
22 | };
23 |
24 | const Toast: React.FC = ({ toast, style }) => {
25 | const { removeToast } = useToast();
26 |
27 | useEffect(() => {
28 | const timer = setTimeout(() => {
29 | removeToast(toast.id);
30 | }, 3000);
31 |
32 | return () => {
33 | clearTimeout(timer);
34 | };
35 | }, [removeToast, toast.id]);
36 |
37 | return (
38 |
43 | {icons[toast.type || 'info']}
44 |
45 |
46 |
{toast.title}
47 | {toast.description &&
{toast.description}
}
48 |
49 |
50 |
53 |
54 | );
55 | };
56 |
57 | export default Toast;
58 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/Toast/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { animated } from 'react-spring';
3 |
4 | interface ToastProps {
5 | type?: 'success' | 'error' | 'info';
6 | hasDescription: number;
7 | }
8 |
9 | const toastTypeVariations = {
10 | info: css`
11 | background: #cbc5ea;
12 | color: #7051dc;
13 | `,
14 | success: css`
15 | background: #e6fffa;
16 | color: #2e656a;
17 | `,
18 | error: css`
19 | background: #fddede;
20 | color: #c53030;
21 | `,
22 | };
23 |
24 | export const Container = styled(animated.div)`
25 | width: 360px;
26 |
27 | position: relative;
28 | padding: 16px 30px 16px 16px;
29 | border-radius: 10px;
30 | box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2);
31 |
32 | display: flex;
33 |
34 | & + div {
35 | margin-top: 16px;
36 | }
37 |
38 | ${({ type }) => toastTypeVariations[type || 'info']};
39 |
40 | > svg {
41 | margin: 4px 12px 0 0;
42 | }
43 |
44 | div {
45 | flex: 1;
46 |
47 | p {
48 | margin-top: 4px;
49 | font-size: 14px;
50 | opacity: 0.8;
51 | line-height: 20px;
52 | }
53 | }
54 |
55 | button {
56 | position: absolute;
57 | right: 16px;
58 | top: 19px;
59 | opacity: 0.6;
60 | border: 0;
61 | background: transparent;
62 | color: inherit;
63 | }
64 |
65 | ${({ hasDescription }) =>
66 | !hasDescription &&
67 | css`
68 | align-items: center;
69 |
70 | svg {
71 | margin-top: 0;
72 | }
73 | `}
74 | `;
75 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useTransition } from 'react-spring';
3 |
4 | import { ToastMessage } from '../../hooks/toast';
5 |
6 | import Toast from './Toast';
7 |
8 | import { Container } from './styles';
9 |
10 | interface ToastContainerProps {
11 | messages: ToastMessage[];
12 | }
13 |
14 | const ToastContainer: React.FC = ({ messages }) => {
15 | const messagesWithTransitions = useTransition(
16 | messages,
17 | message => message.id,
18 | {
19 | from: { right: '-120%', opacity: 0 },
20 | enter: { right: '0%', opacity: 1 },
21 | leave: { right: '-120%', opacity: 0 },
22 | },
23 | );
24 |
25 | return (
26 |
27 | {messagesWithTransitions.map(({ item: message, key, props }) => (
28 |
29 | ))}
30 |
31 | );
32 | };
33 |
34 | export default ToastContainer;
35 |
--------------------------------------------------------------------------------
/src/components/ToastContainer/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | position: absolute;
5 | right: 0;
6 | top: 0;
7 | padding: 30px;
8 | overflow: hidden;
9 | `;
10 |
--------------------------------------------------------------------------------
/src/components/Tooltip/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container } from './styles';
4 |
5 | interface TooltipProps {
6 | title: string;
7 | className?: string;
8 | }
9 |
10 | const Tooltip: React.FC = ({
11 | title,
12 | className = '',
13 | children,
14 | }) => {
15 | return (
16 |
17 | {children}
18 | {title}
19 |
20 | );
21 | };
22 |
23 | export default Tooltip;
24 |
--------------------------------------------------------------------------------
/src/components/Tooltip/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | position: relative;
5 |
6 | span {
7 | width: 160px;
8 | background: #7051dc;
9 | padding: 8px;
10 | border-radius: 4px;
11 | font-size: 14px;
12 | font-weight: 500;
13 | opacity: 0;
14 | transition: opacity 0.45s;
15 | visibility: hidden;
16 | position: absolute;
17 | bottom: calc(100% + 12px);
18 | left: 50%;
19 | transform: translateX(-50%);
20 | color: #312e38;
21 |
22 | &::before {
23 | border-style: solid;
24 | border-color: #7051dc transparent;
25 | border-width: 6px 6px 0 6px;
26 | top: 100%;
27 | position: absolute;
28 | left: 50%;
29 | transform: translateX(-50%);
30 | content: '';
31 | }
32 | }
33 |
34 | &:hover span {
35 | opacity: 1;
36 | visibility: visible;
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/config/chart.ts:
--------------------------------------------------------------------------------
1 | import { ChartOptions } from 'chart.js';
2 |
3 | export default {
4 | line: {
5 | options: {
6 | responsive: true,
7 | layout: {
8 | padding: 0,
9 | },
10 | legend: {
11 | display: false,
12 | position: 'bottom',
13 | labels: {
14 | usePointStyle: true,
15 | padding: 16,
16 | },
17 | },
18 | elements: {
19 | point: {
20 | radius: 0,
21 | backgroundColor: '#7051dc',
22 | },
23 | line: {
24 | tension: 0.4,
25 | borderWidth: 4,
26 | borderColor: '#7051dc',
27 | backgroundColor: 'transparent',
28 | borderCapStyle: 'rounded',
29 | },
30 | rectangle: {
31 | backgroundColor: '#ff9000',
32 | },
33 | arc: {
34 | backgroundColor: '#7051dc',
35 | borderColor: '#fff',
36 | borderWidth: 4,
37 | },
38 | },
39 | tooltips: {
40 | enabled: true,
41 | mode: 'index',
42 | intersect: false,
43 | },
44 | } as ChartOptions,
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/hooks/auth.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useCallback, useContext, useState } from 'react';
2 | import { addMinutes } from 'date-fns';
3 |
4 | import api from '../services/api';
5 |
6 | interface AuthState {
7 | user: User;
8 | token: string;
9 | }
10 |
11 | interface User {
12 | id: string;
13 | name: string;
14 | email: string;
15 | avatar_url: string;
16 | }
17 |
18 | interface Attempt {
19 | id: string;
20 | created_at: Date;
21 | endsIn: Date;
22 | }
23 |
24 | export interface ISignInCredentials {
25 | email: string;
26 | password: string;
27 | }
28 |
29 | interface AuthContextData {
30 | user: User;
31 | attempts: Attempt[];
32 | signIn(data: ISignInCredentials): Promise;
33 | signOut(): void;
34 | clearAttempts(): void;
35 | }
36 |
37 | const AuthContext = createContext({} as AuthContextData);
38 |
39 | const AuthProvider: React.FC = ({ children }) => {
40 | const [attempts, setAttempts] = useState(() => {
41 | const storaged = localStorage.getItem('@GabrielTeodoro:attempts');
42 |
43 | if (storaged) {
44 | const parsedStorage = JSON.parse(storaged);
45 |
46 | return parsedStorage;
47 | }
48 |
49 | return [] as Attempt[];
50 | });
51 |
52 | const [data, setData] = useState(() => {
53 | const token = localStorage.getItem('@GabrielTeodoro:token');
54 | const user = localStorage.getItem('@GabrielTeodoro:user');
55 |
56 | if (token && user) {
57 | return {
58 | token,
59 | user: JSON.parse(user),
60 | };
61 | }
62 |
63 | return {} as AuthState;
64 | });
65 |
66 | const signIn = useCallback(
67 | async ({ email, password }: ISignInCredentials) => {
68 | try {
69 | const response = await api.post('sessions', {
70 | email,
71 | password,
72 | });
73 |
74 | const { user, token } = response.data;
75 |
76 | localStorage.setItem('@GabrielTeodoro:user', JSON.stringify(user));
77 | localStorage.setItem('@GabrielTeodoro:token', token);
78 |
79 | setData({ user, token });
80 | } catch (err) {
81 | const attempt = {
82 | id: String(Math.random()),
83 | created_at: new Date(),
84 | endsIn: addMinutes(new Date(), 5),
85 | };
86 |
87 | localStorage.setItem(
88 | '@GabrielTeodoro:attempts',
89 | JSON.stringify([...attempts, attempt]),
90 | );
91 |
92 | setAttempts([...attempts, attempt]);
93 |
94 | throw new Error(err);
95 | }
96 | },
97 | [attempts, setAttempts],
98 | );
99 |
100 | const signOut = useCallback(() => {
101 | localStorage.removeItem('@GabrielTeodoro:user');
102 | localStorage.removeItem('@GabrielTeodoro:token');
103 |
104 | setData({} as AuthState);
105 | }, []);
106 |
107 | const clearAttempts = useCallback(() => {
108 | console.log('limpando');
109 |
110 | localStorage.removeItem('@GabrielTeodoro:attempts');
111 | setAttempts([]);
112 | }, []);
113 |
114 | return (
115 |
126 | {children}
127 |
128 | );
129 | };
130 |
131 | function useAuth(): AuthContextData {
132 | const context = useContext(AuthContext);
133 |
134 | if (!context) {
135 | throw new Error('useAuth must be used within a AuthProvider');
136 | }
137 |
138 | return context;
139 | }
140 |
141 | export { AuthProvider, useAuth };
142 |
--------------------------------------------------------------------------------
/src/hooks/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { AuthProvider } from './auth';
4 | import { ToastProvider } from './toast';
5 | import { ThemeProvider } from './theme';
6 |
7 | const AppProvider: React.FC = ({ children }) => {
8 | return (
9 |
10 |
11 | {children}
12 |
13 |
14 | );
15 | };
16 |
17 | export default AppProvider;
18 |
--------------------------------------------------------------------------------
/src/hooks/theme.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useCallback, useContext, useState } from 'react';
2 | import { ThemeProvider as Provider } from 'styled-components';
3 |
4 | import dark from '../styles/themes/dark';
5 | import light from '../styles/themes/light';
6 |
7 | interface ITheme {
8 | title: string;
9 |
10 | colors: {
11 | primary: string;
12 | error: string;
13 | warning: string;
14 | success: string;
15 |
16 | background: string;
17 | backgroundSecundary: string;
18 |
19 | text: string;
20 | muted: string;
21 | };
22 | }
23 |
24 | interface ThemeContextData {
25 | theme: ITheme;
26 | toggleTheme(): void;
27 | }
28 |
29 | const ThemeContext = createContext({} as ThemeContextData);
30 |
31 | const ThemeProvider: React.FC = ({ children }) => {
32 | const [theme, setTheme] = useState(() => {
33 | const storaged = localStorage.getItem('@GabrielTeodoro:theme');
34 |
35 | if (storaged) {
36 | return JSON.parse(storaged);
37 | }
38 |
39 | return dark;
40 | });
41 |
42 | const toggleTheme = useCallback(() => {
43 | localStorage.setItem(
44 | '@GabrielTeodoro:theme',
45 | JSON.stringify(theme.title === 'light' ? dark : light),
46 | );
47 |
48 | setTheme(theme.title === 'light' ? dark : light);
49 | }, [theme, setTheme]);
50 |
51 | return (
52 |
53 | {children}
54 |
55 | );
56 | };
57 |
58 | function useTheme(): ThemeContextData {
59 | const context = useContext(ThemeContext);
60 |
61 | if (!context) {
62 | throw new Error('useTheme must be used within a ThemeProvider');
63 | }
64 |
65 | return context;
66 | }
67 |
68 | export { ThemeProvider, useTheme };
69 |
--------------------------------------------------------------------------------
/src/hooks/toast.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useCallback, useContext, useState } from 'react';
2 | import { uuid } from 'uuidv4';
3 |
4 | import ToastContainer from '../components/ToastContainer';
5 |
6 | export interface ToastMessage {
7 | id: string;
8 | type?: 'success' | 'error' | 'info';
9 | title: string;
10 | description?: string;
11 | }
12 |
13 | interface ToastContextData {
14 | addToast(message: Omit): void;
15 | removeToast(id: string): void;
16 | }
17 |
18 | const ToastContext = createContext({} as ToastContextData);
19 |
20 | const ToastProvider: React.FC = ({ children }) => {
21 | const [messages, setMessages] = useState([]);
22 |
23 | const addToast = useCallback(
24 | ({ type, title, description }: Omit) => {
25 | const id = uuid();
26 |
27 | const toast = {
28 | id,
29 | type,
30 | title,
31 | description,
32 | };
33 |
34 | setMessages(state => [...state, toast]);
35 | },
36 | [],
37 | );
38 |
39 | const removeToast = useCallback((id: string) => {
40 | setMessages(state => state.filter(message => message.id !== id));
41 | }, []);
42 |
43 | return (
44 |
47 | {children}
48 |
49 |
50 | );
51 | };
52 |
53 | function useToast(): ToastContextData {
54 | const context = useContext(ToastContext);
55 |
56 | if (!context) {
57 | throw new Error('useToast must be used within a ToastProvider');
58 | }
59 |
60 | return context;
61 | }
62 |
63 | export { ToastProvider, useToast };
64 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root'),
11 | );
12 |
--------------------------------------------------------------------------------
/src/modules/dashboard/components/Card/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType, AreaHTMLAttributes } from 'react';
2 | import { IconBaseProps } from 'react-icons';
3 | import { FiArrowUp, FiArrowDown } from 'react-icons/fi';
4 |
5 | import { Container } from './styles';
6 |
7 | interface ICardProps extends AreaHTMLAttributes {
8 | title?: string;
9 | description?: string;
10 | icon?: ComponentType;
11 | color?: string;
12 | isUp?: boolean;
13 | isDown?: boolean;
14 | percentage?: string;
15 | width?: number;
16 | height?: number;
17 | }
18 |
19 | const Card: React.FC = ({
20 | title,
21 | icon: Icon,
22 | color,
23 | description,
24 | percentage,
25 | isUp,
26 | isDown,
27 | children,
28 | style,
29 | }) => {
30 | return (
31 |
32 | {children ? (
33 | <>{children}>
34 | ) : (
35 | <>
36 |
37 | {title}
38 | {Icon && }
39 |
40 |
52 | >
53 | )}
54 |
55 | );
56 | };
57 |
58 | export default Card;
59 |
--------------------------------------------------------------------------------
/src/modules/dashboard/components/Card/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | flex: 1;
5 | background: ${({ theme }) => theme.colors.backgroundSecundary};
6 | border-radius: 4px;
7 | padding: 20px 30px;
8 |
9 | & + div {
10 | margin-left: 25px;
11 | }
12 |
13 | header,
14 | footer {
15 | display: flex;
16 | align-items: center;
17 | justify-content: space-between;
18 | margin-bottom: 30px;
19 | }
20 |
21 | header {
22 | > div {
23 | display: flex;
24 | align-items: center;
25 |
26 | span {
27 | color: #a8a8b3;
28 | }
29 |
30 | svg {
31 | margin-left: 10px;
32 | }
33 | }
34 | }
35 |
36 | footer {
37 | margin-top: 30px;
38 |
39 | span {
40 | color: #a8a8b3;
41 | }
42 |
43 | div {
44 | display: flex;
45 | align-items: center;
46 |
47 | svg {
48 | margin-left: 10px;
49 | }
50 | }
51 | }
52 |
53 | canvas {
54 | margin-top: 30px;
55 | }
56 | `;
57 |
--------------------------------------------------------------------------------
/src/modules/dashboard/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useRef, useEffect } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { FiSearch, FiBell, FiLogOut } from 'react-icons/fi';
4 | import { formatDistance, parseISO } from 'date-fns';
5 | import ptBR from 'date-fns/locale/pt-BR';
6 |
7 | import { useAuth } from '../../../../hooks/auth';
8 |
9 | import api from '../../../../services/api';
10 |
11 | import { useClickOutside } from '../../../../utils/getClickOutside';
12 |
13 | import {
14 | Container,
15 | UseSearch,
16 | UseNotifications,
17 | NotificationsList,
18 | NotificationItem,
19 | NotificationEmpty,
20 | UseProfile,
21 | } from './styles';
22 |
23 | import notfound from '../../../../assets/not-found.svg';
24 |
25 | interface IHeaderProps {
26 | title: string;
27 | }
28 |
29 | interface INotification {
30 | id: string;
31 | content: string;
32 | created_at: string;
33 | read: boolean;
34 | hourFormatted: string;
35 | }
36 |
37 | const Header: React.FC = ({ title }) => {
38 | const notificationsRef = useRef(null);
39 |
40 | const { user, signOut } = useAuth();
41 |
42 | const [isOpenNotifications, setIsOpenNotifications] = useState(false);
43 | const [notifications, setNotifications] = useState([]);
44 |
45 | useEffect(() => {
46 | api.get('notifications').then(response => {
47 | const data = response.data
48 | .filter(notification => !notification.read)
49 | .map(notification => {
50 | const hourFormatted = formatDistance(
51 | parseISO(notification.created_at),
52 | new Date(),
53 | {
54 | locale: ptBR,
55 | },
56 | );
57 |
58 | return {
59 | ...notification,
60 | hourFormatted,
61 | };
62 | });
63 |
64 | setNotifications(data);
65 | });
66 | }, []);
67 |
68 | const handleToggleNotifications = useCallback(() => {
69 | setIsOpenNotifications(!isOpenNotifications);
70 | }, [isOpenNotifications, setIsOpenNotifications]);
71 |
72 | const handleReadNotification = useCallback(
73 | async (id: string) => {
74 | const response = await api.put(`notifications/${id}`);
75 |
76 | const { data } = response;
77 |
78 | const notificationsUpdatted = notifications
79 | .map(notification => (notification.id === id ? data : notification))
80 | .filter(notification => !notification.read);
81 |
82 | setNotifications(notificationsUpdatted);
83 | },
84 | [setNotifications, notifications],
85 | );
86 |
87 | useClickOutside(
88 | notificationsRef,
89 | () => handleToggleNotifications(),
90 | isOpenNotifications,
91 | );
92 |
93 | return (
94 |
95 |
96 |
{title}
97 |
98 |
99 |
102 |
103 |
104 |
105 |
106 |
0}
109 | >
110 |
113 |
114 |
115 |
116 | {isOpenNotifications && (
117 | <>
118 | {notifications.length > 0 ? (
119 |
120 | {notifications.map(notification => (
121 | handleReadNotification(notification.id)}
123 | >
124 | {notification.content}
125 |
126 |
{notification.hourFormatted}
127 |
128 | {!notification.read &&
}
129 |
130 |
131 | ))}
132 |
133 | ) : (
134 |
135 |
136 | Não encontramos nada...
137 |
138 | Não encontramos nenhuma notificação que não tenha sido
139 | marcada como lida.
140 |
141 |
142 | )}
143 | >
144 | )}
145 |
146 |
147 |
148 |
149 | {user.name}
150 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | );
162 | };
163 |
164 | export default Header;
165 |
--------------------------------------------------------------------------------
/src/modules/dashboard/components/Header/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { shade } from 'polished';
3 |
4 | interface IUseNotificationsProps {
5 | hasNotifications: boolean;
6 | }
7 |
8 | export const Container = styled.header`
9 | width: 100%;
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 |
14 | > div {
15 | display: flex;
16 | align-items: center;
17 |
18 | button {
19 | display: flex;
20 | align-items: center;
21 |
22 | background: transparent;
23 | border: 0;
24 | }
25 |
26 | h1 {
27 | margin-right: 25px;
28 | color: #f4ede8;
29 | }
30 |
31 | > span {
32 | margin-right: 25px;
33 | font-weight: 500;
34 | transition: opacity 0.2s;
35 |
36 | &:hover {
37 | opacity: 0.8;
38 | }
39 | }
40 | }
41 | `;
42 |
43 | export const UseSearch = styled.div`
44 | display: flex;
45 | align-items: center;
46 |
47 | background: #4b2db2;
48 | padding: 10px 20px;
49 | border-radius: 4px;
50 | margin-right: 25px;
51 |
52 | input {
53 | background: transparent;
54 | border: 0;
55 | margin-left: 15px;
56 | color: #f4ede8;
57 |
58 | &::placeholder {
59 | color: #a8a8b3;
60 | }
61 | }
62 | `;
63 |
64 | export const UseNotifications = styled.div`
65 | position: relative;
66 | cursor: pointer;
67 |
68 | > span {
69 | opacity: ${({ hasNotifications }) => (hasNotifications ? 1 : 0)};
70 |
71 | width: 10px;
72 | height: 10px;
73 | border-radius: 50%;
74 | background: #4b2db2;
75 | position: absolute;
76 |
77 | right: 2px;
78 | top: 0;
79 | }
80 | `;
81 |
82 | export const NotificationsList = styled.ul`
83 | position: absolute;
84 | background: ${({ theme }) => theme.colors.background};
85 | width: 350px;
86 | margin-top: 30px;
87 | padding: 20px 25px;
88 | left: calc(50% - 175px);
89 | border-radius: 4px;
90 |
91 | &::before {
92 | content: '';
93 | position: absolute;
94 | left: calc(50% - 20px);
95 | top: -20px;
96 | width: 0;
97 | height: 0;
98 | border-left: 20px solid transparent;
99 | border-right: 20px solid transparent;
100 | border-bottom: 20px solid ${({ theme }) => theme.colors.background};
101 | }
102 | `;
103 |
104 | export const NotificationItem = styled.li`
105 | padding: 10px;
106 | border-radius: 10px;
107 | transition: all 0.2s;
108 |
109 | & + li {
110 | margin-top: 10px;
111 | border-top: 0.5px solid rgba(0, 0, 0, 0.1);
112 | }
113 |
114 | &:hover {
115 | margin-top: 5px;
116 | background: ${({ theme }) => theme.colors.primary};
117 | }
118 |
119 | p {
120 | color: ${({ theme }) => theme.colors.text};
121 | font-size: 16px;
122 | margin-bottom: 10px;
123 | }
124 |
125 | > div {
126 | display: flex;
127 | align-items: center;
128 | justify-content: space-between;
129 |
130 | span {
131 | color: #a8a8b3;
132 | font-size: 14px;
133 | margin-bottom: 10px;
134 | }
135 |
136 | div {
137 | height: 10px;
138 | width: 10px;
139 | background: #4b2db2;
140 | border-radius: 50%;
141 | }
142 | }
143 | `;
144 |
145 | export const NotificationEmpty = styled.div`
146 | position: absolute;
147 | background: ${({ theme }) => theme.colors.background};
148 | width: 350px;
149 | margin-top: 30px;
150 | padding: 20px 25px;
151 | left: calc(50% - 175px);
152 | border-radius: 4px;
153 |
154 | display: grid;
155 | place-items: center;
156 |
157 | &::before {
158 | content: '';
159 | position: absolute;
160 | left: calc(50% - 20px);
161 | top: -20px;
162 | width: 0;
163 | height: 0;
164 | border-left: 20px solid transparent;
165 | border-right: 20px solid transparent;
166 | border-bottom: 20px solid ${({ theme }) => theme.colors.background};
167 | }
168 |
169 | img {
170 | width: 75px;
171 | height: 75px;
172 | }
173 |
174 | strong {
175 | color: ${({ theme }) => theme.colors.muted};
176 | font-size: 16px;
177 | margin-top: 15px;
178 | }
179 |
180 | span {
181 | color: ${({ theme }) => theme.colors.text};
182 | font-size: 12px;
183 | max-width: 400px;
184 | text-align: center;
185 | }
186 | `;
187 |
188 | export const UseProfile = styled.div`
189 | display: flex;
190 | align-items: center;
191 |
192 | img {
193 | width: 70px;
194 | height: 70px;
195 | border-radius: 50%;
196 | margin-right: 10px;
197 | border: 4px solid #4b2db2;
198 | transition: opacity 0.2s;
199 |
200 | &:hover {
201 | opacity: 0.8;
202 | }
203 | }
204 |
205 | div {
206 | display: flex;
207 | flex-direction: column;
208 | align-items: flex-end;
209 | margin-right: 20px;
210 |
211 | strong {
212 | color: #f4ede8;
213 | }
214 |
215 | > button {
216 | display: flex;
217 | align-items: center;
218 |
219 | color: ${({ theme }) => theme.colors.error};
220 | font-size: 14px;
221 | font-weight: 500;
222 | margin-bottom: 10px;
223 | transition: color 0.2s;
224 |
225 | svg {
226 | margin-left: 10px;
227 | }
228 |
229 | &:hover {
230 | color: ${({ theme }) => shade(0.15, theme.colors.error)};
231 | }
232 | }
233 | }
234 | `;
235 |
--------------------------------------------------------------------------------
/src/modules/dashboard/components/Sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | FiHome,
4 | FiMoon,
5 | FiMail,
6 | FiSettings,
7 | FiClipboard,
8 | FiLayers,
9 | FiSun,
10 | } from 'react-icons/fi';
11 | import { shade } from 'polished';
12 |
13 | import { useTheme } from '../../../../hooks/theme';
14 |
15 | import {
16 | Container,
17 | Navigation,
18 | NavigationItem,
19 | NavigationItemLink,
20 | ThemeSwitch,
21 | } from './styles';
22 |
23 | import favicon from '../../../../assets/favicon.svg';
24 |
25 | const Sidebar: React.FC = () => {
26 | const { theme, toggleTheme } = useTheme();
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
74 |
75 |
76 | }
77 | uncheckedIcon={
78 |
88 |
89 |
90 | }
91 | height={30}
92 | width={60}
93 | handleDiameter={20}
94 | offColor={shade(0.15, theme.colors.primary)}
95 | onColor="#f4ede8"
96 | />
97 |
98 |
99 | );
100 | };
101 |
102 | export default Sidebar;
103 |
--------------------------------------------------------------------------------
/src/modules/dashboard/components/Sidebar/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import Switch from 'react-switch';
5 |
6 | export const Container = styled.aside`
7 | position: fixed;
8 |
9 | max-width: 100px;
10 | width: 100%;
11 | background: ${({ theme }) => theme.colors.backgroundSecundary};
12 |
13 | height: 100vh;
14 |
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | justify-content: space-between;
19 |
20 | padding: 50px 0;
21 |
22 | img {
23 | width: 50px;
24 | height: 50px;
25 | }
26 | `;
27 |
28 | export const Navigation = styled.nav``;
29 |
30 | export const NavigationItem = styled.li`
31 | cursor: pointer;
32 | transition: all 0.2s;
33 |
34 | & + li {
35 | margin-top: 40px;
36 | }
37 |
38 | &:hover {
39 | opacity: 0.8;
40 | transform: scale(1.2);
41 | }
42 | `;
43 |
44 | export const NavigationItemLink = styled(NavLink).attrs({
45 | activeStyle: {
46 | color: '#7051dc',
47 | },
48 | })`
49 | color: #a8a8b3;
50 | `;
51 |
52 | export const ThemeSwitch = styled(Switch)``;
53 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Authenticate/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useCallback, useMemo } from 'react';
2 | import { FiMail, FiLock, FiArrowLeft } from 'react-icons/fi';
3 | import { useHistory, Link } from 'react-router-dom';
4 | import { isAfter, parseISO } from 'date-fns';
5 |
6 | import { Form } from '@unform/web';
7 | import { FormHandles } from '@unform/core';
8 | import * as Yup from 'yup';
9 |
10 | import { ISignInCredentials, useAuth } from '../../../../hooks/auth';
11 | import { useToast } from '../../../../hooks/toast';
12 |
13 | import getValidationErrors from '../../../../utils/getValidationErrors';
14 |
15 | import Input from '../../../../components/Input';
16 | import Button from '../../../../components/Button';
17 |
18 | import logo from '../../../../assets/logo.svg';
19 | import authenticateBackground from '../../../../assets/authenticate-background.svg';
20 | import Alert from '../../../../components/Alert';
21 |
22 | const Authenticate: React.FC = () => {
23 | const formRef = useRef(null);
24 |
25 | const { addToast } = useToast();
26 | const { signIn, attempts, clearAttempts } = useAuth();
27 |
28 | const history = useHistory();
29 |
30 | const handleSubmit = useCallback(
31 | async (data: ISignInCredentials) => {
32 | try {
33 | formRef.current?.setErrors([]);
34 |
35 | const schema = Yup.object().shape({
36 | email: Yup.string()
37 | .required('E-mail obrigatório')
38 | .email('Digite um e-mail válido'),
39 | password: Yup.string().required('Senha obrigatória'),
40 | });
41 |
42 | await schema.validate(data, {
43 | abortEarly: false,
44 | });
45 |
46 | await signIn({
47 | email: data.email,
48 | password: data.password,
49 | });
50 |
51 | clearAttempts();
52 |
53 | addToast({
54 | type: 'success',
55 | title: 'Autenticado com sucesso!',
56 | description: `Seja bem vindo novamente!`,
57 | });
58 |
59 | history.push('/dashboard/statistics');
60 | } catch (err) {
61 | if (err instanceof Yup.ValidationError) {
62 | const errors = getValidationErrors(err);
63 |
64 | formRef.current?.setErrors(errors);
65 |
66 | return;
67 | }
68 |
69 | addToast({
70 | type: 'error',
71 | title: 'Erro na autenticação',
72 | description:
73 | 'Ocorreu um erro ao realizar a autenticação, tente novamente.',
74 | });
75 | }
76 | },
77 | [addToast, signIn, clearAttempts, history],
78 | );
79 |
80 | const canTryAgain = useMemo(() => {
81 | if (attempts.length >= 3) {
82 | const attempt = attempts[attempts.length - 1];
83 |
84 | const { endsIn } = attempt;
85 |
86 | if (isAfter(new Date(), parseISO(String(endsIn)))) {
87 | clearAttempts();
88 |
89 | return true;
90 | }
91 |
92 | return false;
93 | }
94 |
95 | return true;
96 | }, [attempts, clearAttempts]);
97 |
98 | return (
99 | <>
100 |
101 |
102 |
103 |
136 |
137 | >
138 | );
139 | };
140 |
141 | export default Authenticate;
142 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useCallback } from 'react';
2 | import { FiHeart, FiUsers, FiClipboard, FiChevronDown } from 'react-icons/fi';
3 | import DayPicker, { DayModifiers } from 'react-day-picker';
4 | import { format, isBefore, isAfter } from 'date-fns';
5 |
6 | import { useTheme } from '../../../../hooks/theme';
7 | import { useClickOutside } from '../../../../utils/getClickOutside';
8 |
9 | import Alert from '../../../../components/Alert';
10 |
11 | import Header from '../../components/Header';
12 | import Card from '../../components/Card';
13 |
14 | import LineChart from './sections/LineChart';
15 |
16 | import { CardGroup, Calendar } from './styles';
17 |
18 | const Main: React.FC = () => {
19 | const { theme } = useTheme();
20 | const calendarRef = useRef(null);
21 |
22 | const [isOpenCalendar, setIsOpenCalendar] = useState(false);
23 |
24 | const [selectedType, setSelectedType] = useState<'start' | 'end'>('start');
25 |
26 | const [, setCurrentMonth] = useState(new Date());
27 | const [selectedDays, setSelectedDays] = useState([
28 | new Date(),
29 | new Date(),
30 | ]);
31 |
32 | const handleDateChange = useCallback(
33 | (day: Date, modifiers: DayModifiers) => {
34 | if (modifiers.available && !modifiers.disabled) {
35 | const [start_at, end_at] = selectedDays;
36 |
37 | if (selectedType === 'end') {
38 | if (isAfter(end_at, day)) {
39 | setSelectedDays([day, end_at]);
40 | setSelectedType('start');
41 | }
42 | }
43 |
44 | if (selectedType === 'start') {
45 | if (isBefore(start_at, day)) {
46 | setSelectedDays([start_at, day]);
47 | setSelectedType('end');
48 | }
49 | }
50 | }
51 | },
52 | [selectedDays, selectedType, setSelectedDays],
53 | );
54 |
55 | const handleMonthChange = useCallback(
56 | (month: Date) => {
57 | setCurrentMonth(month);
58 | },
59 | [setCurrentMonth],
60 | );
61 |
62 | const handleToggleCalendar = useCallback(() => {
63 | setIsOpenCalendar(!isOpenCalendar);
64 | }, [setIsOpenCalendar, isOpenCalendar]);
65 |
66 | useClickOutside(calendarRef, () => handleToggleCalendar(), isOpenCalendar);
67 |
68 | return (
69 | <>
70 |
71 |
72 |
73 |
81 |
82 |
90 |
91 |
99 |
100 |
101 | Você precisa adquirir o Netlify para ver os acessos da página.
102 |
103 |
104 |
105 |
106 | Crescimento da página
107 |
108 |
109 |
117 |
118 | {isOpenCalendar && (
119 |
143 | )}
144 |
145 |
146 |
147 |
148 |
149 |
150 | >
151 | );
152 | };
153 |
154 | export default Main;
155 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Main/sections/LineChart/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { Line } from 'react-chartjs-2';
3 | import { format, eachDayOfInterval } from 'date-fns';
4 |
5 | import { ChartData } from 'react-chartjs-2';
6 | import { ChartData as IChartData } from 'chart.js';
7 |
8 | import chartConfig from '../../../../../../config/chart';
9 |
10 | interface ILineChartProps {
11 | data?: ChartData;
12 | height?: number;
13 | selectedDays: Date[];
14 | }
15 |
16 | const data = {
17 | datasets: [
18 | {
19 | label: 'Usuários',
20 | fill: false,
21 | lineTension: 0.5,
22 | backgroundColor: '#7051dc',
23 | borderColor: '#7051dc',
24 | borderCapStyle: 'butt',
25 | borderDash: [],
26 | borderDashOffset: 0.0,
27 | borderJoinStyle: 'round',
28 | pointBorderColor: '#7051dc',
29 | pointBackgroundColor: '#fff',
30 | pointBorderWidth: 1,
31 | pointHoverRadius: 5,
32 | pointHoverBackgroundColor: '#7051dc',
33 | pointHoverBorderColor: 'rgba(220,220,220,1)',
34 | pointHoverBorderWidth: 2,
35 | pointRadius: 1,
36 | pointHitRadius: 10,
37 | data: [65, 59, 80, 81, 56, 55, 40, 50, 60, 30],
38 | },
39 | ],
40 | } as ChartData;
41 |
42 | const LineChart: React.FC = ({ height, selectedDays }) => {
43 | const labelSelectedDays = useMemo(() => {
44 | const [start, end] = selectedDays;
45 |
46 | const parsedDays = eachDayOfInterval({
47 | start,
48 | end,
49 | });
50 |
51 | return parsedDays.map(date => format(date, 'dd/MM'));
52 | }, [selectedDays]);
53 |
54 | return (
55 |
60 | );
61 | };
62 |
63 | export default LineChart;
64 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Main/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { shade } from 'polished';
3 |
4 | export const CardGroup = styled.div`
5 | flex: 1;
6 | display: flex;
7 | align-items: center;
8 | margin-top: 25px;
9 | `;
10 |
11 | export const Calendar = styled.div`
12 | width: 380px;
13 | position: relative;
14 |
15 | display: flex;
16 | flex-direction: column;
17 | align-items: flex-end;
18 | justify-content: flex-end;
19 |
20 | button {
21 | background: transparent;
22 | border: 0;
23 | margin-left: auto;
24 | }
25 |
26 | .DayPicker {
27 | position: absolute;
28 | background: ${({ theme }) => theme.colors.background};
29 | border-radius: 10px;
30 |
31 | top: 50px;
32 | }
33 |
34 | .DayPicker-wrapper {
35 | padding-bottom: 0;
36 | }
37 |
38 | .DayPicker,
39 | .DayPicker-Month {
40 | width: 100%;
41 | }
42 |
43 | .DayPicker-Month {
44 | border-collapse: separate;
45 | border-spacing: 8px;
46 | margin: 16px;
47 | }
48 |
49 | .DayPicker-Day {
50 | width: 40px;
51 | height: 40px;
52 | }
53 |
54 | .DayPicker-Day--available:not(.DayPicker-Day--outside) {
55 | background: ${({ theme }) => theme.colors.backgroundSecundary};
56 | border-radius: 10px;
57 | color: ${({ theme }) => theme.colors.text};
58 | }
59 |
60 | .DayPicker:not(.DayPicker--interactionDisabled)
61 | .DayPicker-Day:not(.DayPicker-Day--disabled):not(.DayPicker-Day--selected):not(.DayPicker-Day--outside):hover {
62 | background: ${({ theme }) => shade(0.2, theme.colors.backgroundSecundary)};
63 | }
64 |
65 | .DayPicker-Day--today {
66 | font-weight: normal;
67 | }
68 |
69 | .DayPicker-Day--disabled {
70 | color: #1c1c2e !important;
71 | background: transparent !important;
72 | }
73 |
74 | .DayPicker-Day--selected {
75 | background: #7051dc !important;
76 | border-radius: 10px;
77 | color: #f4ede8 !important;
78 | }
79 | `;
80 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/FileList/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container, FileInfo } from './styles';
4 |
5 | interface IFileProps {
6 | name: string;
7 | readableSize: string;
8 | }
9 |
10 | interface IFileListProps {
11 | files: IFileProps[];
12 | }
13 |
14 | const FileList: React.FC = ({ files }: IFileListProps) => {
15 | return (
16 |
17 | {files.map(uploadedFile => (
18 |
19 |
20 |
21 | {uploadedFile.name}
22 | {uploadedFile.readableSize}
23 |
24 |
25 |
26 | ))}
27 |
28 | );
29 | };
30 |
31 | export default FileList;
32 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/FileList/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.ul`
4 | margin-top: 20px;
5 |
6 | li {
7 | display: flex;
8 | justify-content: space-between;
9 | align-items: center;
10 | color: ${({ theme }) => theme.colors.primary};
11 |
12 | & + li {
13 | margin-top: 15px;
14 | }
15 | }
16 | `;
17 |
18 | export const FileInfo = styled.div`
19 | display: flex;
20 | align-items: center;
21 | justify-content: space-between;
22 | flex: 1;
23 |
24 | button {
25 | border: 0;
26 | background: transparent;
27 | color: ${({ theme }) => theme.colors.error};
28 | margin-left: 5px;
29 | cursor: pointer;
30 | }
31 |
32 | div {
33 | display: flex;
34 | flex-direction: column;
35 | justify-content: space-between;
36 |
37 | span {
38 | font-size: 12px;
39 | color: #999;
40 | margin-top: 5px;
41 | }
42 | }
43 | `;
44 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Shimmer from '../../../../../components/Shimmer';
4 |
5 | import { Table, TableItem } from '../styles';
6 |
7 | const items = [1, 2, 3];
8 |
9 | const Skeleton: React.FC = () => {
10 | return (
11 |
12 |
13 |
14 | Nome |
15 | Github |
16 | Demonstração |
17 | Curtidas |
18 | Ações |
19 |
20 |
21 |
22 | {items.map(item => (
23 |
24 |
25 |
26 | |
27 |
28 |
29 | |
30 |
31 |
32 | |
33 |
34 |
35 | |
36 |
37 |
38 | |
39 |
40 | ))}
41 |
42 |
43 | );
44 | };
45 |
46 | export default Skeleton;
47 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/Upload/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | import Dropzone from 'react-dropzone';
4 | import { DropContainer, UploadMessage } from './styles';
5 |
6 | interface UploadProps {
7 | onUpload: Function;
8 | }
9 |
10 | const Upload: React.FC = ({ onUpload }: UploadProps) => {
11 | function renderDragMessage(
12 | isDragActive: boolean,
13 | isDragRejest: boolean,
14 | ): ReactNode {
15 | if (!isDragActive) {
16 | return (
17 | Selecione ou arraste o arquivo aqui.
18 | );
19 | }
20 |
21 | if (isDragRejest) {
22 | return Arquivo não suportado;
23 | }
24 |
25 | return Solte o arquivo aqui;
26 | }
27 |
28 | return (
29 | <>
30 | onUpload(files)}>
31 | {({ getRootProps, getInputProps, isDragActive, isDragReject }): any => (
32 |
37 |
38 | {renderDragMessage(isDragActive, isDragReject)}
39 |
40 | )}
41 |
42 | >
43 | );
44 | };
45 |
46 | export default Upload;
47 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/Upload/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
2 |
3 | interface UploadProps {
4 | isDragActive: boolean;
5 | isDragReject: boolean;
6 | refKey?: string;
7 | [key: string]: any;
8 | type?: 'error' | 'success' | 'default';
9 | }
10 |
11 | const dragActive = css`
12 | border-color: #12a454;
13 | `;
14 |
15 | const dragReject = css`
16 | border-color: #e83f5b;
17 | `;
18 |
19 | export const DropContainer = styled.div.attrs({
20 | className: 'dropzone',
21 | })`
22 | border: 1.5px dashed ${({ theme }) => theme.colors.primary};
23 | border-radius: 5px;
24 | cursor: pointer;
25 |
26 | transition: height 0.2s ease;
27 |
28 | ${(props: UploadProps): false | FlattenSimpleInterpolation =>
29 | props.isDragActive && dragActive}
30 |
31 | ${(props: UploadProps): false | FlattenSimpleInterpolation =>
32 | props.isDragReject && dragReject}
33 | `;
34 |
35 | const messageColors = {
36 | default: '#7051dc',
37 | error: '#e83f5b',
38 | success: '#12a454',
39 | };
40 |
41 | export const UploadMessage = styled.p`
42 | display: flex;
43 | font-size: 16px;
44 | line-height: 24px;
45 | padding: 48px 0;
46 | transition: opacity 0.2s;
47 |
48 | color: ${({ type }: UploadProps) => messageColors[type || 'default']};
49 |
50 | &:hover {
51 | opacity: 0.8;
52 | }
53 |
54 | justify-content: center;
55 | align-items: center;
56 | `;
57 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState, useCallback } from 'react';
2 | import {
3 | FiTag,
4 | FiAlignCenter,
5 | FiGithub,
6 | FiLink,
7 | FiEdit,
8 | FiTrash2,
9 | FiEye,
10 | } from 'react-icons/fi';
11 | import * as Yup from 'yup';
12 |
13 | import filesize from 'filesize';
14 |
15 | import { Form } from '@unform/web';
16 | import { FormHandles } from '@unform/core';
17 |
18 | import { useToast } from '../../../../hooks/toast';
19 |
20 | import getValidationErrors from '../../../../utils/getValidationErrors';
21 |
22 | import api from '../../../../services/api';
23 |
24 | import Input from '../../../../components/Input';
25 | import Button from '../../../../components/Button';
26 |
27 | import Header from '../../components/Header';
28 | import Card from '../../components/Card';
29 |
30 | import Upload from './Upload';
31 | import FileList from './FileList';
32 | import Skeleton from './Skeleton';
33 |
34 | import {
35 | Content,
36 | CardGroup,
37 | InputGroup,
38 | Table,
39 | TableItem,
40 | ActionItem,
41 | } from './styles';
42 |
43 | interface IFileProps {
44 | file: File;
45 | name: string;
46 | readableSize: string;
47 | }
48 |
49 | interface IProject {
50 | name: string;
51 | description: string;
52 | repository: string;
53 | demonstration: string;
54 | likes: number;
55 | files: IFileProps[];
56 | }
57 |
58 | const Projects: React.FC = () => {
59 | const formRef = useRef(null);
60 | const { addToast } = useToast();
61 |
62 | const [loading, setLoading] = useState(true);
63 | const [projects, setProjects] = useState([]);
64 | const [uploadedFiles, setUploadedFiles] = useState([]);
65 |
66 | useEffect(() => {
67 | api.get('projects').then(response => {
68 | setProjects(response.data);
69 | setLoading(false);
70 | });
71 | }, []);
72 |
73 | const handleSubmit = useCallback(
74 | async (data: IProject) => {
75 | setLoading(true);
76 |
77 | try {
78 | formRef.current?.setErrors([]);
79 |
80 | const schema = Yup.object().shape({
81 | name: Yup.string().required('Nome obrigatório'),
82 | description: Yup.string().required('Descrição obrigatória'),
83 | repository: Yup.string().required('Repositório obrigatório'),
84 | demonstration: Yup.string().required('Demonstração obrigatória'),
85 | });
86 |
87 | await schema.validate(data, {
88 | abortEarly: false,
89 | });
90 |
91 | const formData = new FormData();
92 |
93 | formData.append('name', data.name);
94 | formData.append('description', data.description);
95 | formData.append('repository', data.repository);
96 | formData.append('demonstration', data.demonstration);
97 |
98 | if (uploadedFiles.length > 0) {
99 | uploadedFiles.map(uploadFile =>
100 | formData.append('files', uploadFile.file),
101 | );
102 | }
103 |
104 | const response = await api.post('projects', formData);
105 |
106 | setProjects([...projects, response.data]);
107 |
108 | addToast({
109 | type: 'success',
110 | title: 'Projeto criado com sucesso',
111 | });
112 | } catch (err) {
113 | if (err instanceof Yup.ValidationError) {
114 | const errors = getValidationErrors(err);
115 |
116 | formRef.current?.setErrors(errors);
117 |
118 | return;
119 | }
120 |
121 | addToast({
122 | type: 'error',
123 | title: 'Erro na criação',
124 | description:
125 | 'Ocorreu um erro ao realizar a criação de um projeto, tente novamente.',
126 | });
127 | } finally {
128 | setLoading(false);
129 | }
130 | },
131 | [addToast, uploadedFiles, projects, setProjects],
132 | );
133 |
134 | const handleSubmitFiles = useCallback(
135 | (files: File[]) => {
136 | const uploadFiles = files.map((file: File) => {
137 | const { name } = file;
138 | const readableSize = filesize(file.size);
139 |
140 | return {
141 | file,
142 | name,
143 | readableSize,
144 | };
145 | });
146 |
147 | setUploadedFiles([...uploadedFiles, ...uploadFiles]);
148 | },
149 | [setUploadedFiles, uploadedFiles],
150 | );
151 |
152 | return (
153 | <>
154 |
155 |
156 |
157 |
158 |
159 |
160 | Criar um novo projeto
161 |
162 |
163 |
187 |
188 |
189 |
190 | Upload de imagens
191 |
192 |
193 |
194 | {!!uploadedFiles.length && }
195 |
196 |
197 |
198 |
199 | Listagem de projetos
200 |
201 | {loading ? (
202 |
203 | ) : (
204 |
205 |
206 |
207 | Nome |
208 | Github |
209 | Demonstração |
210 | Curtidas |
211 | Ações |
212 |
213 |
214 |
215 | {projects.map(project => (
216 |
217 | {project.name} |
218 |
219 | {project.repository}
220 | |
221 |
222 |
223 | {project.demonstration}
224 |
225 | |
226 | {project.likes} |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 | |
239 |
240 | ))}
241 |
242 |
243 | )}
244 |
245 |
246 | >
247 | );
248 | };
249 |
250 | export default Projects;
251 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Projects/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | interface IActionItemProps {
4 | color: string;
5 | }
6 |
7 | export const Content = styled.main`
8 | margin-top: 30px;
9 | `;
10 |
11 | export const CardGroup = styled.div`
12 | display: flex;
13 | `;
14 |
15 | export const InputGroup = styled.div`
16 | display: flex;
17 | align-items: center;
18 | `;
19 |
20 | export const Table = styled.table`
21 | width: 100%;
22 | text-align: left;
23 |
24 | border-collapse: separate;
25 | border-spacing: 0px 10px;
26 | border-style: hidden;
27 |
28 | display: table;
29 |
30 | th {
31 | font-weight: 500;
32 |
33 | &:first-child {
34 | padding-left: 20px;
35 | }
36 |
37 | &:last-child {
38 | padding-right: 20px;
39 | text-align: right;
40 | }
41 | }
42 | `;
43 |
44 | export const TableItem = styled.tr`
45 | background: ${({ theme }) => theme.colors.background};
46 | transition: background 0.2s;
47 |
48 | cursor: pointer;
49 |
50 | td {
51 | padding: 20px 0;
52 | font-size: 14px;
53 | color: ${({ theme }) => theme.colors.muted};
54 |
55 | a {
56 | text-decoration: none;
57 | color: ${({ theme }) => theme.colors.muted};
58 | transition: color 0.2s;
59 |
60 | &:hover {
61 | color: ${({ theme }) => theme.colors.text};
62 | }
63 | }
64 |
65 | &:first-child {
66 | padding-left: 20px;
67 | border-radius: 8px 0 0 8px;
68 | }
69 |
70 | &:last-child {
71 | padding-right: 20px;
72 | border-radius: 0 8px 8px 0;
73 | text-align: right;
74 | }
75 | }
76 | `;
77 |
78 | export const ActionItem = styled.button`
79 | background: ${({ color }) => color};
80 | border: 0;
81 | padding: 5px 10px;
82 | border-radius: 4px;
83 | transition: opacity 0.2s;
84 |
85 | & + button {
86 | margin-left: 5px;
87 | }
88 |
89 | &:hover {
90 | opacity: 0.8;
91 | }
92 | `;
93 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Tenders/Skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Shimmer from '../../../../../components/Shimmer';
4 |
5 | import { Table, TableItem } from '../styles';
6 |
7 | const items = [1, 2, 3];
8 |
9 | const Skeleton: React.FC = () => {
10 | return (
11 |
12 |
13 |
14 | Nome |
15 | E-mail |
16 | Já possui layout? |
17 | Nº de Páginas |
18 | Status |
19 |
20 |
21 |
22 | {items.map(item => (
23 |
24 |
25 |
26 | |
27 |
28 |
29 | |
30 |
31 |
32 | |
33 |
34 |
35 | |
36 |
37 |
38 | |
39 |
40 | ))}
41 |
42 |
43 | );
44 | };
45 |
46 | export default Skeleton;
47 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Tenders/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback } from 'react';
2 | import { format, parseISO } from 'date-fns';
3 |
4 | import { useToast } from '../../../../hooks/toast';
5 |
6 | import {
7 | Table,
8 | TableItem,
9 | TenderStatus,
10 | TenderDetail,
11 | UseSettings,
12 | UseSettingsItem,
13 | Error,
14 | } from './styles';
15 |
16 | import Button from '../../../../components/Button';
17 | import AnswerModal from '../../../../components/AnswerModal';
18 |
19 | import Header from '../../components/Header';
20 | import Card from '../../components/Card';
21 |
22 | import Skeleton from './Skeleton';
23 |
24 | import api from '../../../../services/api';
25 |
26 | import notfound from '../../../../assets/not-found.svg';
27 |
28 | interface ITender {
29 | id: string;
30 | name: string;
31 | email: string;
32 | message: string;
33 | layout: boolean;
34 | pages: string;
35 | status: 'pending' | 'accept' | 'dismiss';
36 | created_at: string;
37 | translattedStatus: string;
38 | dateFormatted: string;
39 | visible: boolean;
40 | }
41 |
42 | const Tenders: React.FC = () => {
43 | const { addToast } = useToast();
44 |
45 | const [loading, setLoading] = useState(true);
46 | const [tenders, setTenders] = useState([]);
47 |
48 | const [selectedStatus, setSelectedStatus] = useState<
49 | 'pending' | 'accept' | 'dismiss'
50 | >('pending');
51 |
52 | useEffect(() => {
53 | async function loadTenders(): Promise {
54 | const response = await api.get('tenders/status', {
55 | params: {
56 | status: selectedStatus,
57 | },
58 | });
59 |
60 | const data = response.data.map(tender => {
61 | const translattedStatus =
62 | tender.status === 'pending'
63 | ? 'Pendente'
64 | : tender.status === 'accept'
65 | ? 'Aceito'
66 | : 'Recusado';
67 |
68 | return {
69 | ...tender,
70 | translattedStatus,
71 | hourFormatted: format(
72 | parseISO(tender.created_at),
73 | 'dd/MM/yyyy HH:mm',
74 | ),
75 | visible: false,
76 | };
77 | });
78 |
79 | setTenders(data);
80 | setLoading(false);
81 | }
82 |
83 | loadTenders();
84 | }, [selectedStatus]);
85 |
86 | const handleSelectStatus = useCallback(
87 | (value: 'pending' | 'accept' | 'dismiss') => {
88 | setSelectedStatus(value);
89 | setLoading(true);
90 | },
91 | [setSelectedStatus],
92 | );
93 |
94 | const handleToggleVisibleTenderDetail = useCallback(
95 | (id: string) => {
96 | const tendersUpdatted = tenders.map(findTender =>
97 | findTender.id === id
98 | ? {
99 | ...findTender,
100 | visible: !findTender.visible,
101 | }
102 | : findTender,
103 | );
104 |
105 | setTenders(tendersUpdatted);
106 | },
107 | [tenders, setTenders],
108 | );
109 |
110 | const handleChangeStatus = useCallback(
111 | async (id: string, status: 'pending' | 'accept' | 'dismiss') => {
112 | try {
113 | const response = await api.put(`tenders/status/${id}`, {
114 | status,
115 | });
116 |
117 | const updatted = response.data;
118 |
119 | Object.assign(updatted, {
120 | translattedStatus: status === 'accept' ? 'Aceito' : 'Recusado',
121 | hourFormatted: format(
122 | parseISO(updatted.created_at),
123 | 'dd/MM/yyyy HH:mm',
124 | ),
125 | visible: false,
126 | });
127 |
128 | setSelectedStatus(status);
129 |
130 | addToast({
131 | type: status === 'accept' ? 'success' : 'info',
132 | title: `Proposta ${status === 'accept' ? 'Aceita' : 'Recusada'}`,
133 | description:
134 | status === 'accept'
135 | ? 'Parabéns! Agora entre em contato com o cliente.'
136 | : 'Você recusou uma proposta, quem sabe da próxima.',
137 | });
138 | } catch (err) {
139 | addToast({
140 | type: 'error',
141 | title: 'Erro ao aceitar proposta',
142 | description:
143 | 'Ocorreu um erro ao aceitar a proposta, tente novamente.',
144 | });
145 | }
146 | },
147 | [addToast],
148 | );
149 |
150 | return (
151 | <>
152 |
153 |
154 |
155 |
156 | Últimas propostas
157 |
158 |
159 | handleSelectStatus('pending')}
162 | >
163 | Pendente
164 |
165 | handleSelectStatus('accept')}
168 | >
169 | Aceito
170 |
171 | handleSelectStatus('dismiss')}
174 | >
175 | Recusado
176 |
177 |
178 |
179 | {loading ? (
180 |
181 | ) : (
182 | <>
183 | {tenders.length > 0 ? (
184 |
185 |
186 |
187 | Nome |
188 | E-mail |
189 | Já possui layout? |
190 | Nº de Páginas |
191 | Status |
192 |
193 |
194 |
195 | {tenders.map(tender => (
196 | <>
197 |
200 | handleToggleVisibleTenderDetail(tender.id)
201 | }
202 | >
203 | {tender.name} |
204 | {tender.email} |
205 | {tender.layout ? 'Sim' : 'Não'} |
206 |
207 | {tender.pages === '+' ? 'Mais de 7' : tender.pages}
208 | |
209 |
210 |
211 | {tender.translattedStatus}
212 |
213 | |
214 |
215 |
216 |
219 | handleToggleVisibleTenderDetail(tender.id)
220 | }
221 | >
222 | Detalhes da proposta
223 |
224 |
225 |
226 | Criado por:
227 | {tender.name}
228 |
229 |
230 | E-mail para contato:
231 | {tender.email}
232 |
233 |
234 | Já possui o layout?
235 | {tender.layout ? 'Sim' : 'Não'}
236 |
237 |
238 | Número de páginas:
239 |
240 | {tender.pages === '+'
241 | ? 'Mais de 7 páginas'
242 | : `${tender.pages} página(s)`}
243 |
244 |
245 |
246 | Mensagem sobre o projeto:
247 |
248 |
249 |
250 |
251 | Status:
252 | {tender.translattedStatus}
253 |
254 | {tender.status === 'pending' && (
255 |
273 | )}
274 |
275 |
276 |
279 |
280 | >
281 | ))}
282 |
283 |
284 | ) : (
285 |
286 |
287 | Não encontramos nada...
288 |
289 | Não encontramos nenhuma proposta de acordo com os filtros
290 | aplicados.
291 |
292 |
293 | )}
294 | >
295 | )}
296 |
297 | >
298 | );
299 | };
300 |
301 | export default Tenders;
302 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/Tenders/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { lighten, darken, shade } from 'polished';
3 |
4 | interface ITenderStatus {
5 | status: 'pending' | 'accept' | 'dismiss';
6 | }
7 |
8 | interface IUseSettingsItemProps {
9 | selected: boolean;
10 | }
11 |
12 | export const Wrapper = styled.div`
13 | display: flex;
14 | position: relative;
15 |
16 | > img {
17 | position: absolute;
18 | top: -50px;
19 | z-index: -1;
20 | width: 100%;
21 | }
22 | `;
23 |
24 | export const Container = styled.div`
25 | flex: 1;
26 | padding: 50px 15px 50px 125px;
27 |
28 | max-width: 1510px;
29 |
30 | > div {
31 | margin-top: 50px;
32 | }
33 | `;
34 |
35 | export const UseSettings = styled.ul`
36 | display: flex;
37 | align-items: center;
38 | `;
39 |
40 | export const UseSettingsItem = styled.li`
41 | background: ${({ selected, theme }) =>
42 | selected ? theme.colors.primary : theme.colors.background};
43 | color: ${({ selected, theme }) => (selected ? '#f4ede8' : theme.colors.text)};
44 | padding: 5px 10px;
45 | border-radius: 4px;
46 | transition: opacity 0.2s;
47 | cursor: pointer;
48 |
49 | font-size: 12px;
50 |
51 | & + li {
52 | margin-left: 10px;
53 | }
54 |
55 | &:hover {
56 | opacity: 0.8;
57 | }
58 | `;
59 |
60 | export const Table = styled.table`
61 | width: 100%;
62 | text-align: left;
63 |
64 | border-collapse: separate;
65 | border-spacing: 0px 10px;
66 | border-style: hidden;
67 |
68 | display: table;
69 |
70 | th {
71 | font-weight: 500;
72 |
73 | &:first-child {
74 | padding-left: 20px;
75 | }
76 |
77 | &:last-child {
78 | padding-right: 20px;
79 | text-align: right;
80 | }
81 | }
82 | `;
83 |
84 | export const TableItem = styled.tr`
85 | background: ${({ theme }) => theme.colors.background};
86 | transition: background 0.2s;
87 |
88 | cursor: pointer;
89 |
90 | td {
91 | padding: 20px 0;
92 | font-size: 14px;
93 | color: ${({ theme }) => theme.colors.muted};
94 | transition: color 0.2s;
95 |
96 | &:first-child {
97 | padding-left: 20px;
98 | border-radius: 8px 0 0 8px;
99 | }
100 |
101 | &:last-child {
102 | padding-right: 20px;
103 | border-radius: 0 8px 8px 0;
104 | text-align: right;
105 | }
106 |
107 | &:hover {
108 | color: ${({ theme }) => theme.colors.text};
109 | }
110 | }
111 |
112 | &:hover {
113 | background: ${({ theme }) => theme.colors.primary};
114 | }
115 | `;
116 |
117 | export const TenderStatus = styled.div`
118 | border-radius: 4px;
119 | font-weight: 500;
120 | text-align: center;
121 | height: 30px;
122 |
123 | padding: 20px 5px;
124 |
125 | display: flex;
126 | align-items: center;
127 | justify-content: center;
128 |
129 | color: ${({ status, theme }) =>
130 | status === 'pending'
131 | ? theme.colors.primary
132 | : status === 'accept'
133 | ? darken(0.23, theme.colors.success)
134 | : theme.colors.error};
135 | background: ${({ status, theme }) =>
136 | status === 'pending'
137 | ? lighten(0.23, theme.colors.primary)
138 | : status === 'accept'
139 | ? lighten(0.23, theme.colors.success)
140 | : lighten(0.23, theme.colors.error)};
141 | `;
142 |
143 | export const TenderDetail = styled.pre`
144 | background: rgba(0, 0, 0, 0.2);
145 | margin-top: 25px;
146 | padding: 20px;
147 | border-radius: 4px;
148 |
149 | div {
150 | display: flex;
151 | flex-direction: column;
152 |
153 | + div {
154 | margin-top: 20px;
155 | }
156 |
157 | > strong {
158 | font-size: 16px;
159 | font-weight: 500;
160 | }
161 |
162 | > span {
163 | font-size: 14px;
164 | color: #87868b;
165 | }
166 |
167 | textarea {
168 | border: 0;
169 | background: transparent;
170 | font-size: 14px;
171 | color: #87868b;
172 | resize: vertical;
173 | max-height: 300px;
174 | }
175 | }
176 |
177 | footer {
178 | display: flex;
179 | align-items: center;
180 |
181 | button + button {
182 | margin-left: 15px;
183 | background: ${({ theme }) => theme.colors.error};
184 |
185 | &:hover {
186 | background: ${({ theme }) => shade(0.2, theme.colors.error)};
187 | }
188 | }
189 | }
190 | `;
191 |
192 | export const Error = styled.div`
193 | flex: 1;
194 | margin: 30px 0;
195 |
196 | display: grid;
197 | place-items: center;
198 |
199 | strong {
200 | color: ${({ theme }) => theme.colors.muted};
201 | font-size: 18px;
202 | margin-top: 15px;
203 | }
204 |
205 | span {
206 | color: ${({ theme }) => theme.colors.text};
207 | font-size: 14px;
208 | max-width: 400px;
209 | text-align: center;
210 | }
211 | `;
212 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/_layouts/auth/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container } from './styles';
4 |
5 | const AuthLayout: React.FC = ({ children }) => {
6 | return {children};
7 | };
8 |
9 | export default AuthLayout;
10 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/_layouts/auth/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | > img {
5 | position: absolute;
6 | top: 0px;
7 | width: 100%;
8 | z-index: -1;
9 | }
10 |
11 | main {
12 | max-width: 600px;
13 | margin: 75px auto 50px;
14 |
15 | form {
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | justify-content: center;
20 |
21 | flex: 1;
22 | background: #1c1c2e;
23 | border-radius: 10px;
24 | padding: 90px;
25 |
26 | > img,
27 | h1 {
28 | margin-bottom: 50px;
29 | }
30 |
31 | button {
32 | margin-top: 35px;
33 | }
34 |
35 | a {
36 | color: #f4ede8;
37 | font-weight: 500;
38 | text-decoration: none;
39 |
40 | display: flex;
41 | align-items: center;
42 |
43 | margin-top: 30px;
44 | transition: opacity 0.2s;
45 |
46 | &:hover {
47 | opacity: 0.8;
48 | }
49 |
50 | svg {
51 | margin-right: 10px;
52 | }
53 | }
54 | }
55 | }
56 | `;
57 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/_layouts/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Sidebar from '../../../components/Sidebar';
4 |
5 | import { Wrapper, Container } from './styles';
6 |
7 | import dashboardBackground from '../../../../../assets/dashboard-background.svg';
8 |
9 | const DashboardLayout: React.FC = ({ children }) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | {children}
17 |
18 | );
19 | };
20 |
21 | export default DashboardLayout;
22 |
--------------------------------------------------------------------------------
/src/modules/dashboard/screens/_layouts/dashboard/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Wrapper = styled.div`
4 | display: flex;
5 | position: relative;
6 |
7 | > img {
8 | position: absolute;
9 | top: -50px;
10 | z-index: -1;
11 | width: 100%;
12 | }
13 | `;
14 |
15 | export const Container = styled.div`
16 | flex: 1;
17 | padding: 50px 15px 50px 125px;
18 |
19 | max-width: 1510px;
20 | `;
21 |
--------------------------------------------------------------------------------
/src/modules/users/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import footerBackground from '../../../../assets/footer-background.svg';
4 |
5 | import { Container } from './styles';
6 |
7 | const Footer: React.FC = () => {
8 | return (
9 |
10 |
11 |
12 |
13 | Gabriel Teodoro ©
14 | Todos os direitos reservados.
15 |
16 |
17 | );
18 | };
19 |
20 | export default Footer;
21 |
--------------------------------------------------------------------------------
/src/modules/users/components/Footer/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.footer`
4 | display: flex;
5 | position: relative;
6 | max-width: 1120px;
7 |
8 | /** Desktop */
9 | @media screen and (min-width: 1200px) {
10 | margin: 450px auto 0;
11 | }
12 |
13 | /** Mobile */
14 | @media screen and (max-width: 430px) {
15 | display: grid;
16 | place-items: center;
17 |
18 | margin: 100px auto 0;
19 |
20 | img {
21 | display: none;
22 | }
23 |
24 | span {
25 | text-align: center;
26 | }
27 | }
28 |
29 | img {
30 | position: absolute;
31 | bottom: 0px;
32 | left: -400px;
33 | z-index: -1;
34 | width: 1913px;
35 | }
36 |
37 | span {
38 | color: #f4ede8;
39 | font-size: 16px;
40 | margin-bottom: 100px;
41 |
42 | a {
43 | color: #a3a3a3;
44 | text-decoration: none;
45 | font-weight: 500;
46 | transition: opacity 0.2s;
47 |
48 | &:hover {
49 | opacity: 0.8;
50 | }
51 | }
52 | }
53 | `;
54 |
--------------------------------------------------------------------------------
/src/modules/users/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-scroll';
3 |
4 | import { Container, Navigation, NavigationItem } from './styles';
5 |
6 | import logo from '../../../../assets/logo.svg';
7 |
8 | const Header: React.FC = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | Sobre mim
17 |
18 |
19 |
20 |
21 | Projetos
22 |
23 |
24 |
25 |
26 | Iniciar um novo projeto
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Header;
35 |
--------------------------------------------------------------------------------
/src/modules/users/components/Header/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | interface INavigationToggleButtonProps {
4 | isOpen: boolean;
5 | }
6 |
7 | export const Container = styled.header`
8 | max-width: 1120px;
9 | margin: 80px auto 0;
10 |
11 | display: flex;
12 | align-items: center;
13 | justify-content: space-between;
14 |
15 | /** Mobile */
16 | @media screen and (max-width: 430px) {
17 | display: flex;
18 | flex-direction: column;
19 | align-items: center;
20 |
21 | padding: 0 25px;
22 | }
23 | `;
24 |
25 | export const Navigation = styled.nav`
26 | display: flex;
27 | align-items: center;
28 |
29 | /** Mobile */
30 | @media screen and (max-width: 430px) {
31 | display: none;
32 | }
33 | `;
34 |
35 | export const NavigationToggleButton = styled.button<
36 | INavigationToggleButtonProps
37 | >`
38 | display: none;
39 | background: transparent;
40 | border: 0;
41 |
42 | ul {
43 | display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
44 | flex-direction: column;
45 | align-items: flex-start;
46 | padding: 15px;
47 | border-radius: 4px;
48 | background: #fff;
49 | margin-top: 10px;
50 |
51 | li {
52 | margin-bottom: 5px;
53 |
54 | a {
55 | color: #7051dc;
56 | font-weight: bold;
57 | }
58 | }
59 | }
60 | `;
61 |
62 | export const NavigationItem = styled.li`
63 | cursor: pointer;
64 | transition: opacity 0.2s;
65 | margin-right: 20px;
66 |
67 | a {
68 | color: #f4ede8;
69 | font-family: 'Poppins', sans-serif;
70 | font-size: 16px;
71 | }
72 |
73 | &:hover {
74 | opacity: 0.8;
75 | }
76 | `;
77 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Header from '../../components/Header';
4 | import Footer from '../../components/Footer';
5 |
6 | import CreateProject from './sections/CreateProject';
7 | import Presentation from './sections/Presentation';
8 | import Profile from './sections/Profile';
9 | import Projects from './sections/Projects';
10 |
11 | import { Container } from './styles';
12 |
13 | const Home: React.FC = () => {
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | >
27 | );
28 | };
29 |
30 | export default Home;
31 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/CreateProject/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useCallback } from 'react';
2 | import { Element } from 'react-scroll';
3 | import { FiUser, FiMail } from 'react-icons/fi';
4 | import * as Yup from 'yup';
5 |
6 | import { FormHandles } from '@unform/core';
7 | import { Form } from '@unform/web';
8 |
9 | import getValidationErrors from '../../../../../../utils/getValidationErrors';
10 |
11 | import api from '../../../../../../services/api';
12 |
13 | import Button from '../../../../../../components/Button';
14 | import Input from '../../../../../../components/Input';
15 | import TextArea from '../../../../../../components/TextArea';
16 | import Alert from '../../../../../../components/Alert';
17 |
18 | import { Container, InputGroup, UseCheckbox, SelectPage } from './styles';
19 |
20 | interface ICreateProjectFormData {
21 | name: string;
22 | email: string;
23 | message: string;
24 | layout: boolean;
25 | pages: string;
26 | }
27 |
28 | const available = ['1', '2', '3', '4', '5', '+'];
29 |
30 | const CreateProject: React.FC = () => {
31 | const formRef = useRef(null);
32 |
33 | const [loading, setLoading] = useState(false);
34 |
35 | const [selectedLayout, setSelectedLayout] = useState(false);
36 | const [selectedPagination, setSelectedPagination] = useState('1');
37 |
38 | const [hasError, setHasError] = useState(false);
39 | const [hasSuccess, setHasSuccess] = useState(false);
40 |
41 | const handleSelectLayout = useCallback(
42 | (value: boolean) => {
43 | setSelectedLayout(value);
44 | },
45 | [setSelectedLayout],
46 | );
47 |
48 | const handleSelectPagination = useCallback(
49 | (value: string) => {
50 | setSelectedPagination(value);
51 | },
52 | [setSelectedPagination],
53 | );
54 |
55 | const handleSubmit = useCallback(
56 | async (data: ICreateProjectFormData) => {
57 | setLoading(true);
58 |
59 | try {
60 | formRef.current?.setErrors([]);
61 |
62 | const schema = Yup.object().shape({
63 | name: Yup.string().required('Nome obrigatório'),
64 | email: Yup.string()
65 | .required('E-mail obrigatório')
66 | .email('Digite um e-mail válido'),
67 | message: Yup.string().required(
68 | 'Digite uma mensagem para descrever o seu projeto',
69 | ),
70 | layout: Yup.boolean().required(),
71 | });
72 |
73 | Object.assign(data, {
74 | layout: selectedLayout,
75 | pages: selectedPagination,
76 | });
77 |
78 | await schema.validate(data, {
79 | abortEarly: false,
80 | });
81 |
82 | await api.post('tenders', data);
83 |
84 | setHasSuccess(true);
85 | } catch (err) {
86 | if (err instanceof Yup.ValidationError) {
87 | const errors = getValidationErrors(err);
88 |
89 | formRef.current?.setErrors(errors);
90 |
91 | return;
92 | }
93 |
94 | setHasSuccess(false);
95 | setHasError(true);
96 | } finally {
97 | setLoading(false);
98 |
99 | setTimeout(() => {
100 | setHasSuccess(false);
101 | setHasError(false);
102 | }, 3000);
103 | }
104 | },
105 | [selectedPagination, selectedLayout],
106 | );
107 |
108 | return (
109 |
110 |
111 | iniciar.projeto
112 |
113 | Vamos tirar sua idéia do papel!
114 |
115 | Conte me um pouco da sua idéia, te retornarei o mais rápido possível.
116 |
117 | Você só pode enviar uma proposta por vez!
118 |
119 |
185 |
186 | );
187 | };
188 |
189 | export default CreateProject;
190 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/CreateProject/styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | interface IUseCheckboxProps {
4 | isChecked: boolean;
5 | }
6 |
7 | interface ISelectPageProps {
8 | isChecked: boolean;
9 | }
10 |
11 | export const Container = styled.section`
12 | display: flex;
13 |
14 | > div {
15 | margin-right: 90px;
16 | padding-top: 30px;
17 |
18 | h1 {
19 | font-size: 50px;
20 | color: #f4ede8;
21 | margin-bottom: 50px;
22 |
23 | &::before {
24 | content: '<';
25 | color: #7051dc;
26 | }
27 |
28 | &::after {
29 | content: '/>';
30 | color: #7051dc;
31 | }
32 | }
33 |
34 | h3 {
35 | font-size: 33px;
36 | color: #f4ede8;
37 | max-width: 260px;
38 | margin-bottom: 25px;
39 | }
40 |
41 | p {
42 | color: #a8a8b3;
43 | font-size: 16px;
44 | max-width: 400px;
45 | margin-bottom: 25px;
46 | }
47 |
48 | span {
49 | background: #cbc5ea;
50 | padding: 10px 15px;
51 | border-radius: 4px;
52 | color: #7051dc;
53 | }
54 | }
55 |
56 | form {
57 | flex: 1;
58 | background: #1c1c2e;
59 | border-radius: 10px;
60 | padding: 90px;
61 |
62 | > div {
63 | margin-bottom: 30px;
64 |
65 | ul {
66 | display: flex;
67 | align-items: center;
68 | margin-top: 20px;
69 | }
70 | }
71 | }
72 |
73 | /** Mobile */
74 | @media screen and (max-width: 430px) {
75 | flex-direction: column;
76 | align-items: center;
77 |
78 | > div {
79 | display: flex;
80 | flex-direction: column;
81 | align-items: center;
82 | margin: -55px 0 0;
83 | padding: 0 25px;
84 |
85 | h1 {
86 | font-size: 36px;
87 | }
88 |
89 | h3 {
90 | font-size: 24px;
91 | text-align: center;
92 | }
93 |
94 | p {
95 | text-align: center;
96 | margin-bottom: 30px;
97 | font-size: 13px;
98 | }
99 | }
100 |
101 | form {
102 | padding: 25px !important;
103 | width: 100%;
104 |
105 | /** Mobile */
106 | @media screen and (max-width: 430px) {
107 | margin-top: 15px;
108 | border-radius: 0;
109 | }
110 | }
111 | }
112 | `;
113 |
114 | export const InputGroup = styled.div`
115 | display: flex;
116 | align-items: center;
117 | margin-top: 20px;
118 | `;
119 |
120 | export const UseCheckbox = styled.label`
121 | display: flex;
122 | align-items: center;
123 |
124 | strong {
125 | cursor: pointer;
126 | height: 24px;
127 | width: 24px;
128 | border-radius: 50%;
129 |
130 | border: 2px solid #7051dc;
131 | position: relative;
132 |
133 | display: flex;
134 | flex-direction: column;
135 | align-items: center;
136 | justify-content: center;
137 |
138 | ${({ isChecked }) =>
139 | isChecked &&
140 | css`
141 | &::after {
142 | content: '';
143 | height: 10px;
144 | width: 10px;
145 | border-radius: 50%;
146 | background: #7051dc;
147 | position: absolute;
148 | }
149 | `};
150 | }
151 |
152 | span {
153 | margin-right: 30px;
154 | }
155 | `;
156 |
157 | export const SelectPage = styled.li`
158 | display: flex;
159 | align-items: center;
160 |
161 | span {
162 | display: flex;
163 | flex-direction: column;
164 | align-items: center;
165 | justify-content: center;
166 |
167 | cursor: pointer;
168 | height: 40px;
169 | width: 40px;
170 | font-weight: 400;
171 |
172 | border: 2px solid #7051dc;
173 | position: relative;
174 |
175 | ${({ isChecked }) =>
176 | isChecked &&
177 | css`
178 | background: #7051dc;
179 | `}
180 |
181 | /** Mobile */
182 | @media screen and (max-width: 430px) {
183 | height: 26px;
184 | width: 26px;
185 | font-size: 12px;
186 | }
187 | }
188 | `;
189 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/Presentation/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Element, Link } from 'react-scroll';
3 | import { FiHelpCircle } from 'react-icons/fi';
4 |
5 | import highBackgroundImg from '../../../../../../assets/high-background.svg';
6 |
7 | import { Container } from './styles';
8 |
9 | const Presentation: React.FC = () => {
10 | return (
11 |
12 |
13 | As melhores tecnologias para as melhores idéias.
14 |
15 | Aplicações fullstack para web e mobile desde o layout até o código.
16 |
17 |
18 |
19 | Iniciar um projeto
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default Presentation;
34 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/Presentation/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { shade } from 'polished';
3 |
4 | export const Container = styled.section`
5 | max-width: 480px;
6 |
7 | display: flex;
8 | align-items: center;
9 |
10 | div {
11 | a {
12 | background: #7051dc;
13 | height: 60px;
14 | border-radius: 4px;
15 | border: 0;
16 | padding: 0 16px;
17 | color: #f4ede8;
18 | width: 100%;
19 | font-weight: 500;
20 | margin-top: 16px;
21 | transition: all 0.2s;
22 | margin-right: 20px;
23 |
24 | cursor: pointer;
25 |
26 | display: flex;
27 | align-items: center;
28 | justify-content: center;
29 |
30 | /** Mobile */
31 | @media screen and (min-width: 1200px) {
32 | & + a {
33 | max-width: 80px;
34 | }
35 | }
36 |
37 | &:hover {
38 | background: ${shade(0.2, '#7051DC')};
39 | }
40 | }
41 |
42 | /** Mobile */
43 | @media screen and (max-width: 430px) {
44 | display: flex;
45 | flex-direction: column;
46 |
47 | a {
48 | margin-left: 20px;
49 | width: 330px;
50 | }
51 | }
52 |
53 | /** Mobile 375px */
54 | @media screen and (max-width: 375px) {
55 | display: flex;
56 | flex-direction: column;
57 |
58 | a {
59 | width: 100%;
60 | }
61 | }
62 |
63 | h1 {
64 | color: #f4ede8;
65 | }
66 |
67 | p {
68 | color: #868693;
69 | margin: 15px 0 35px;
70 | }
71 |
72 | > div {
73 | display: flex;
74 | align-items: center;
75 | }
76 | }
77 |
78 | /** Mini Desktop */
79 | @media screen and (max-width: 1024px) {
80 | padding: 0 25px;
81 |
82 | img {
83 | display: none;
84 | }
85 | }
86 |
87 | /** Mobile */
88 | @media screen and (max-width: 430px) {
89 | h1,
90 | p {
91 | text-align: center;
92 | }
93 |
94 | h1 {
95 | font-size: 29px;
96 | }
97 |
98 | p {
99 | font-size: 15px;
100 | }
101 | }
102 |
103 | /** Desktop */
104 | @media screen and (min-width: 1200px) {
105 | img {
106 | position: absolute;
107 | top: 0px;
108 | right: -50px;
109 | z-index: -1;
110 | }
111 |
112 | h1 {
113 | font-size: 36px;
114 | }
115 |
116 | p {
117 | font-size: 20px;
118 | }
119 | }
120 | `;
121 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/Profile/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FiGithub, FiLinkedin } from 'react-icons/fi';
3 |
4 | import { Container, UseProfile } from './styles';
5 |
6 | import profileImg from '../../../../../../assets/images/profile.png';
7 | import profileBackgroundImg from '../../../../../../assets/profile-background.svg';
8 |
9 | const Profile: React.FC = () => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 | sobre.mim
29 |
30 |
31 | Há muito tempo atrás, um simples menino chamado{' '}
32 | Gabriel Teodoro
33 | , aos seus 13 anos de idade aprendia a desenvolver seus primeiros
34 | sistemas, desde então esse simples menino cresceu apaixonado por
35 | programação.
36 |
37 |
38 | Hoje esse menino chamado Gabriel cresceu e virou desenvolvedor web e
39 | mobile, apaixonado por React, React Native, Node.js e todo o
40 | ecosistema por volta de todas essas tecnologias.
41 |
42 |
43 | Esse jovem hoje em dia busca evoluir ainda mais em sua área, em busca
44 | de novas oportunidades para continuar progredindo como desenvolvedor.
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default Profile;
52 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/Profile/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { Element } from 'react-scroll';
3 | import { shade } from 'polished';
4 |
5 | export const Container = styled(Element).attrs({
6 | name: 'profile',
7 | })`
8 | margin-top: 300px;
9 | padding-top: 30px;
10 |
11 | display: flex;
12 | align-items: center;
13 |
14 | > img {
15 | position: absolute;
16 | left: 0;
17 | z-index: -1;
18 | }
19 |
20 | /** Mobile */
21 | @media screen and (max-width: 430px) {
22 | margin-top: 100px !important;
23 | padding: 0 15px;
24 |
25 | flex-direction: column;
26 | justify-content: center;
27 |
28 | > img {
29 | display: none;
30 | }
31 | }
32 | `;
33 |
34 | export const UseProfile = styled.div`
35 | max-width: 420px;
36 |
37 | & + div {
38 | margin-left: 170px;
39 | }
40 |
41 | img {
42 | width: 330px;
43 | height: 450px;
44 | border-radius: 8px;
45 | }
46 |
47 | div {
48 | display: flex;
49 | align-items: center;
50 |
51 | a {
52 | background: #7051dc;
53 | height: 60px;
54 | border-radius: 4px;
55 | border: 0;
56 | padding: 0 16px;
57 | color: #f4ede8;
58 | width: 100%;
59 | font-weight: 500;
60 | margin-top: 16px;
61 | transition: all 0.2s;
62 |
63 | display: flex;
64 | align-items: center;
65 | justify-content: center;
66 |
67 | & + a {
68 | margin-left: 20px;
69 | }
70 |
71 | &:hover {
72 | background: ${shade(0.2, '#7051DC')};
73 | }
74 | }
75 |
76 | /** Mobile */
77 | @media screen and (max-width: 430px) {
78 | flex-direction: column;
79 |
80 | a + a {
81 | margin-left: 0 !important;
82 | }
83 | }
84 | }
85 |
86 | h1 {
87 | font-size: 50px;
88 | color: #f4ede8;
89 |
90 | &::before {
91 | content: '<';
92 | color: #7051dc;
93 | }
94 |
95 | &::after {
96 | content: '/>';
97 | color: #7051dc;
98 | }
99 | }
100 |
101 | p {
102 | color: #a8a8b3;
103 | font-size: 16px;
104 | margin-top: 60px;
105 |
106 | a {
107 | color: #7051dc;
108 | text-decoration: none;
109 | font-weight: 500;
110 | transition: opacity 0.2s;
111 |
112 | &:hover {
113 | opacity: 0.8;
114 | }
115 | }
116 | }
117 |
118 | /** Mobile */
119 | @media screen and (max-width: 430px) {
120 | text-align: center;
121 | margin: 0 !important;
122 |
123 | h1 {
124 | margin-top: 60px;
125 | font-size: 36px;
126 | }
127 |
128 | p {
129 | font-size: 11px;
130 | }
131 | }
132 | `;
133 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/Projects/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useMemo } from 'react';
2 | import { Element } from 'react-scroll';
3 | import { FiChevronsRight, FiChevronsLeft } from 'react-icons/fi';
4 |
5 | import api from '../../../../../../services/api';
6 |
7 | import Button from '../../../../../../components/Button';
8 |
9 | import { Container, UseProject, Pagination, PaginationItem } from './styles';
10 |
11 | interface IFile {
12 | url: string;
13 | }
14 |
15 | interface IProject {
16 | id: string;
17 | name: string;
18 | description: string;
19 | repository: string;
20 | demonstration: string;
21 | thumbnail_url: string;
22 | files: IFile[];
23 | }
24 |
25 | const Projects: React.FC = () => {
26 | const [projects, setProjects] = useState([]);
27 | const [selectedProject, setSelectedProject] = useState(
28 | {} as IProject,
29 | );
30 |
31 | useEffect(() => {
32 | async function loadProjects(): Promise {
33 | const response = await api.get('projects');
34 |
35 | const data = response.data.map(project => {
36 | const { files } = project;
37 |
38 | return {
39 | ...project,
40 | thumbnail_url: files[0].url,
41 | };
42 | });
43 |
44 | if (data.length > 0) {
45 | setSelectedProject(data[0]);
46 | }
47 |
48 | setProjects(data);
49 | }
50 |
51 | loadProjects();
52 | }, []);
53 |
54 | const handleSelectProject = useCallback(
55 | (id: string) => {
56 | const findProject = projects.find(project => project.id === id);
57 |
58 | if (findProject && selectedProject.id !== id) {
59 | setSelectedProject(findProject);
60 | }
61 | },
62 | [setSelectedProject, selectedProject, projects],
63 | );
64 |
65 | const nextProjectPosition = useMemo(() => {
66 | const projectIndex = projects.findIndex(
67 | project => project.id === selectedProject.id,
68 | );
69 |
70 | return projects[projectIndex + 1];
71 | }, [projects, selectedProject]);
72 |
73 | const backProjectPosition = useMemo(() => {
74 | const projectIndex = projects.findIndex(
75 | project => project.id === selectedProject.id,
76 | );
77 |
78 | return projects[projectIndex - 1];
79 | }, [projects, selectedProject]);
80 |
81 | return (
82 |
83 |
84 |
meus.projetos
85 |
86 | {selectedProject && (
87 |
88 | {selectedProject.name}
89 |
90 | {selectedProject.description}
91 | Acessar projeto
92 |
93 | )}
94 |
95 |
96 |
102 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | {projects.map(project => (
115 | handleSelectProject(project.id)}
119 | />
120 | ))}
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | export default Projects;
128 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/sections/Projects/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | interface IPaginationItemProps {
4 | isSelected?: boolean;
5 | }
6 |
7 | export const Container = styled.section`
8 | display: flex;
9 | align-items: center;
10 |
11 | > div {
12 | padding-top: 30px;
13 |
14 | & + div {
15 | margin-left: 80px;
16 | }
17 |
18 | display: flex;
19 | flex-direction: column;
20 | align-items: center;
21 |
22 | img {
23 | width: 640px;
24 | height: 600px;
25 | border-radius: 10px;
26 | }
27 |
28 | h1 {
29 | color: #f4ede8;
30 |
31 | &::before {
32 | content: '<';
33 | color: #7051dc;
34 | }
35 |
36 | &::after {
37 | content: '/>';
38 | color: #7051dc;
39 | }
40 | }
41 | }
42 |
43 | /** Mobile */
44 | @media screen and (max-width: 430px) {
45 | flex-direction: column;
46 |
47 | #projects {
48 | img,
49 | ul {
50 | display: none;
51 | }
52 | }
53 |
54 | > div {
55 | h1 {
56 | font-size: 36px;
57 | }
58 | }
59 | }
60 |
61 | /** Desktop */
62 | @media screen and (min-width: 1200px) {
63 | h1 {
64 | font-size: 50px;
65 | }
66 | }
67 | `;
68 |
69 | export const UseProject = styled.div`
70 | display: flex;
71 | flex-direction: column;
72 | margin-bottom: 50px;
73 |
74 | h3 {
75 | font-size: 35px;
76 | margin: 50px 0;
77 |
78 | /** Mobile */
79 | @media screen and (max-width: 430px) {
80 | font-size: 25px;
81 | }
82 | }
83 |
84 | p {
85 | color: #a8a8b3;
86 | font-size: 16px;
87 | text-align: center;
88 | }
89 |
90 | a {
91 | display: none;
92 | color: #7051dc;
93 | text-decoration: none;
94 | font-weight: 500;
95 | margin-top: 5px;
96 | transition: opacity 0.2s;
97 |
98 | &:hover {
99 | opacity: 0.8;
100 | }
101 |
102 | /** Mobile */
103 | @media screen and (max-width: 430px) {
104 | display: block;
105 | }
106 | }
107 |
108 | /** Mobile */
109 | @media screen and (max-width: 430px) {
110 | padding: 0 25px;
111 | }
112 | `;
113 |
114 | export const Pagination = styled.ul`
115 | display: flex;
116 | align-items: center;
117 |
118 | button {
119 | height: 40px;
120 |
121 | & + button {
122 | margin-left: 15px;
123 | }
124 | }
125 | `;
126 |
127 | export const PaginationItem = styled.li`
128 | height: 4px;
129 | width: 40px;
130 | background: ${({ isSelected }) => (isSelected ? '#7051dc' : '#36364A')};
131 | margin: 20px 30px 20px 0;
132 | transition: opacity 0.2s;
133 | cursor: pointer;
134 |
135 | &:hover {
136 | opacity: 0.8;
137 | }
138 | `;
139 |
--------------------------------------------------------------------------------
/src/modules/users/screens/Main/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | max-width: 1120px;
5 | margin: 0 auto;
6 |
7 | section {
8 | margin-top: 200px;
9 |
10 | /** Mobile */
11 | @media screen and (max-width: 764px) {
12 | margin-top: 100px !important;
13 | }
14 | }
15 | `;
16 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/routes/Route.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Route as ReactDOMRoute,
4 | RouteProps as ReactDOMRouteProps,
5 | Redirect,
6 | } from 'react-router-dom';
7 |
8 | import { useAuth } from '../hooks/auth';
9 |
10 | interface RouteProps extends ReactDOMRouteProps {
11 | isPrivate?: boolean;
12 | component: React.ComponentType;
13 | layout?: React.ComponentType;
14 | }
15 |
16 | const Route: React.FC = ({
17 | isPrivate = false,
18 | component: Component,
19 | layout: Layout,
20 | ...rest
21 | }) => {
22 | const { user } = useAuth();
23 |
24 | const isSigned = !!user;
25 |
26 | return (
27 | {
30 | return !isPrivate ? (
31 | <>
32 | {Layout ? (
33 |
34 |
35 |
36 | ) : (
37 |
38 | )}
39 | >
40 | ) : (
41 | <>
42 | {isSigned ? (
43 | <>
44 | {Layout ? (
45 |
46 |
47 |
48 | ) : (
49 |
50 | )}
51 | >
52 | ) : (
53 |
58 | )}
59 | >
60 | );
61 | }}
62 | />
63 | );
64 | };
65 |
66 | export default Route;
67 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Switch } from 'react-router-dom';
3 |
4 | import Route from './Route';
5 |
6 | import Main from '../modules/users/screens/Main';
7 |
8 | import AuthLayout from '../modules/dashboard/screens/_layouts/auth';
9 | import DashboardLayout from '../modules/dashboard/screens/_layouts/dashboard';
10 |
11 | import Dashboard from '../modules/dashboard/screens/Main';
12 | import Authenticate from '../modules/dashboard/screens/Authenticate';
13 | import Projects from '../modules/dashboard/screens/Projects';
14 | import Tenders from '../modules/dashboard/screens/Tenders';
15 |
16 | const Routes: React.FC = () => {
17 | return (
18 |
19 |
20 |
21 |
26 |
27 |
34 |
41 |
48 |
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Routes;
56 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const api = axios.create({
4 | baseURL: process.env.REACT_APP_API_URL,
5 | });
6 |
7 | export default api;
8 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 |
3 | import 'react-day-picker/lib/style.css';
4 |
5 | export default createGlobalStyle`
6 | * {
7 | margin: 0;
8 | padding: 0;
9 | box-sizing: border-box;
10 | outline: 0;
11 | }
12 |
13 | body {
14 | background: ${({ theme }) => theme.colors.background};
15 | color: ${({ theme }) => theme.colors.text};
16 | -webkit-font-smoothing: antialiased;
17 | }
18 |
19 | body, input, button {
20 | font-family: 'Poppins', 'Roboto', 'Ubuntu', sans-serif;
21 | font-size: 16px;
22 | }
23 |
24 | h1, h2, h3, h4, h5, h6, strong {
25 | font-family: 'Ubuntu', sans-serif;
26 | font-weight: bold;
27 | }
28 |
29 | button {
30 | cursor: pointer;
31 | }
32 |
33 | ul, nav {
34 | list-style: none;
35 | }
36 | `;
37 |
--------------------------------------------------------------------------------
/src/styles/themes/dark.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | title: 'dark',
3 |
4 | colors: {
5 | primary: '#7051dc',
6 | error: '#c53030',
7 | warning: '#ff9000',
8 | success: '#61e294',
9 |
10 | background: '#08081A',
11 | backgroundSecundary: '#1c1c2e',
12 |
13 | text: '#f4ede8',
14 | muted: '#a8a8b3',
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/styles/themes/light.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | title: 'light',
3 |
4 | colors: {
5 | primary: '#7051dc',
6 | error: '#c53030',
7 | warning: '#ff9000',
8 | success: '#61e294',
9 |
10 | background: '#EFF3F9',
11 | backgroundSecundary: '#FFF',
12 |
13 | text: '#4F4F4F',
14 | muted: '#a8a8b3',
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/getClickOutside.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback, RefObject } from 'react';
2 |
3 | const useClickOutside = (
4 | ref: RefObject,
5 | toggleElement: Function,
6 | isOpen: boolean,
7 | ): void => {
8 | const handleClickOutside = useCallback(
9 | (event: MouseEvent) => {
10 | if (isOpen) {
11 | if (ref.current && !ref.current.contains(event.target as Node)) {
12 | toggleElement();
13 | }
14 | }
15 | },
16 | [isOpen, ref, toggleElement],
17 | );
18 |
19 | useEffect(() => {
20 | document.addEventListener('mousedown', handleClickOutside);
21 |
22 | return () => {
23 | document.removeEventListener('mousedown', handleClickOutside);
24 | };
25 | }, [handleClickOutside]);
26 | };
27 |
28 | export { useClickOutside };
29 |
--------------------------------------------------------------------------------
/src/utils/getValidationErrors.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from 'yup';
2 |
3 | interface Errors {
4 | [key: string]: string;
5 | }
6 |
7 | export default function getValidationErrors(err: ValidationError): Errors {
8 | const validationErrors: Errors = {};
9 |
10 | err.inner.forEach(error => {
11 | validationErrors[error.path] = error.message;
12 | });
13 |
14 | return validationErrors;
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react"
17 | },
18 | "include": ["src"]
19 | }
--------------------------------------------------------------------------------