(
28 |
29 |
30 |
31 | )}
32 | />
33 | >
34 | );
35 | }
36 |
37 | RouteWrapper.propTypes = {
38 | isPrivate: PropTypes.bool,
39 | component: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired,
40 | };
41 |
42 | RouteWrapper.defaultProps = {
43 | isPrivate: false,
44 | };
45 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true,
4 | jest: true,
5 | browser: true
6 | },
7 | extends: ["airbnb", "prettier", "prettier/react"],
8 | globals: {
9 | Atomics: "readonly",
10 | SharedArrayBuffer: "readonly",
11 | __DEV__: true
12 | },
13 | parserOptions: {
14 | ecmaFeatures: {
15 | jsx: true
16 | },
17 | ecmaVersion: 2018,
18 | sourceType: "module"
19 | },
20 | plugins: ["react", "jsx-a11y", "import", "react-hooks", "prettier"],
21 | rules: {
22 | "prettier/prettier": "error",
23 | "react/jsx-filename-extension": ["error", { extensions: [".js", ".jsx"] }],
24 | "import/prefer-default-export": "off",
25 | "no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
26 | "react/jsx-one-expression-per-line": "off",
27 | "global-require": "off",
28 | "react-native/no-raw-text": "off",
29 | "no-param-reassign": "off",
30 | "no-underscore-dangle": "off",
31 | camelcase: "off",
32 | "no-console": ["error", { allow: ["tron"] }],
33 | "react-hooks/rules-of-hooks": "error",
34 | "react-hooks/exhaustive-deps": "warn"
35 | },
36 | settings: {
37 | "import/resolver": {
38 | "alias": {
39 | "map": [["@", "./src"]],
40 | },
41 | },
42 | },
43 | };
--------------------------------------------------------------------------------
/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 |
5 | import { Container, Content, Profile } from './Header_Styles';
6 |
7 | import logo from '@/assets/img/gobarber_logo_purple.svg';
8 | import Notifications from '../Notifications';
9 |
10 | export default function Header() {
11 | const profile = useSelector(state => state.user.profile);
12 |
13 | return (
14 |
15 |
16 |
22 |
23 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Header/Header_Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.header`
4 | background: #fff;
5 | padding: 0 30px;
6 | `;
7 |
8 | export const Content = styled.section`
9 | height: 64px;
10 | max-width: 900px;
11 | margin: 0 auto;
12 | display: flex;
13 | justify-content: space-between;
14 | align-items: center;
15 |
16 | nav {
17 | display: flex;
18 | align-items: center;
19 |
20 | img {
21 | margin-right: 20px;
22 | padding-right: 20px;
23 | border-right: 1px solid #eee;
24 | }
25 |
26 | a {
27 | font-weight: bold;
28 | color: #7159c1;
29 | }
30 | }
31 |
32 | aside {
33 | display: flex;
34 | align-items: center;
35 | }
36 | `;
37 |
38 | export const Profile = styled.section`
39 | display: flex;
40 | margin-left: 20px;
41 | padding-left: 20px;
42 | border-left: 1px solid #eee;
43 |
44 | div {
45 | text-align: right;
46 | margin-right: 10px;
47 |
48 | strong {
49 | display: block;
50 | color: #333;
51 | }
52 |
53 | a {
54 | display: block;
55 | margin-top: 2px;
56 | font-size: 12px;
57 | color: #999;
58 | }
59 | }
60 | img {
61 | width: 32px;
62 | height: 32px;
63 | border-radius: 50%;
64 | }
65 | `;
66 |
--------------------------------------------------------------------------------
/src/pages/LogIn/LogIn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import { Form, Input } from '@rocketseat/unform';
5 | import * as Yup from 'yup';
6 |
7 | import { logInRequest } from '@/store/modules/auth/actions';
8 |
9 | import logo from '@/assets/img/gobarber_logo.svg';
10 |
11 | const schema = Yup.object().shape({
12 | email: Yup.string()
13 | .email('Invalid e-mail address')
14 | .required('E-mail is required'),
15 | password: Yup.string().required('Password is required'),
16 | });
17 |
18 | export default function LogIn() {
19 | const dispatch = useDispatch();
20 | const loading = useSelector(state => state.auth.loading);
21 |
22 | function handleSubmit({ email, password }) {
23 | dispatch(logInRequest(email, password));
24 | }
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
32 |
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/pages/SignUp/SignUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import { Form, Input } from '@rocketseat/unform';
5 | import * as Yup from 'yup';
6 |
7 | import logo from '@/assets/img/gobarber_logo.svg';
8 | import { signUpRequest } from '@/store/modules/auth/actions';
9 |
10 | const schema = Yup.object().shape({
11 | name: Yup.string().required('Please enter your name'),
12 | email: Yup.string()
13 | .email('Invalid e-mail address')
14 | .required('E-mail is required'),
15 | password: Yup.string()
16 | .min(6, 'Password must have at least 6 characters')
17 | .required('Password is required'),
18 | });
19 |
20 | export default function SignUp() {
21 | const dispatch = useDispatch();
22 |
23 | function handleSubmit({ name, email, password }) {
24 | dispatch(signUpRequest(name, email, password));
25 | }
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 |
33 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/Profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Input } from '@rocketseat/unform';
3 | import { useSelector, useDispatch } from 'react-redux';
4 |
5 | import { Container } from './Profile_Styles';
6 | import { updateProfileRequest } from '@/store/modules/user/actions';
7 | import AvatarInput from './AvatarInput';
8 | import { logOut } from '@/store/modules/auth/actions';
9 |
10 | export default function Profile() {
11 | const profile = useSelector(state => state.user.profile);
12 | const dispatch = useDispatch();
13 |
14 | function handleSubmit(data) {
15 | dispatch(updateProfileRequest(data));
16 | }
17 |
18 | function handleLogOut() {
19 | dispatch(logOut());
20 | }
21 |
22 | return (
23 |
24 |
36 |
37 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/Profile/AvatarInput/AvatarInput.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { useField } from '@rocketseat/unform';
3 |
4 | import { Container } from './AvatarInput_Styles';
5 | import api from '@/services/api';
6 |
7 | export default function AvatarInput({ profile }) {
8 | const { defaultValue, registerField } = useField('avatar');
9 |
10 | const [preview, setPreview] = useState(defaultValue && defaultValue.url);
11 | const [file, setFile] = useState(defaultValue && defaultValue.id);
12 |
13 | const ref = useRef();
14 |
15 | useEffect(() => {
16 | if (ref.current) {
17 | registerField({
18 | name: 'avatar_id',
19 | ref: ref.current,
20 | path: 'dataset.file',
21 | });
22 | }
23 | }, [ref, registerField]);
24 |
25 | async function handleChange(e) {
26 | const data = new FormData();
27 |
28 | data.append('file', e.target.files[0]);
29 |
30 | const response = await api.post('files', data);
31 |
32 | const { id, url } = response.data;
33 |
34 | setFile(id);
35 | setPreview(url);
36 | }
37 |
38 | return (
39 |
40 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/assets/img/gobarber_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/_layouts/AuthLayout/AuthLayout_Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { lighten, darken } from 'polished';
3 |
4 | export const Wrapper = styled.div`
5 | height: 100%;
6 | background: linear-gradient(-45deg, #7159c1, #ab59c1);
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | `;
11 |
12 | export const Content = styled.div`
13 | width: 100%;
14 | max-width: 315px;
15 | text-align: center;
16 |
17 | form {
18 | display: flex;
19 | flex-direction: column;
20 | margin-top: 30px;
21 |
22 | input {
23 | background: rgba(0, 0, 0, 0.15);
24 | border-radius: 4px;
25 | height: 44px;
26 | padding: 0 15px;
27 | color: #fff;
28 | margin: 0 0 10px;
29 |
30 | &::placeholder {
31 | color: #bbb;
32 | }
33 | }
34 |
35 | span {
36 | background: #f64c75;
37 | color: #6d2335;
38 | padding: 4px 8px;
39 | margin: -12px 0 10px;
40 | border-radius: 0 0 4px 4px;
41 | font-weight: bold;
42 | }
43 |
44 | button {
45 | background: #3b9eff;
46 | margin: 5px 0 0;
47 | padding: 12px;
48 | height: 44px;
49 | font-weight: bold;
50 | border-radius: 4px;
51 | color: #15385d;
52 | transition: background 150ms ease-in-out;
53 |
54 | &:hover {
55 | background: ${lighten(0.03, '#3b9eff')};
56 | }
57 |
58 | &:active {
59 | background: ${darken(0.05, '#3b9eff')};
60 | }
61 | }
62 |
63 | a {
64 | background: none;
65 | color: #fff;
66 | margin-top: 10px;
67 | font-size: 16px;
68 | opacity: 0.7;
69 | transition: all 150ms ease-in-out;
70 |
71 | &:hover {
72 | opacity: 1;
73 | }
74 |
75 | }
76 | }
77 | }
78 | `;
79 |
--------------------------------------------------------------------------------
/src/pages/Profile/Profile_Styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { lighten, darken } from 'polished';
3 |
4 | export const Container = styled.section`
5 | max-width: 600px;
6 | margin: 50px auto;
7 |
8 | form {
9 | display: flex;
10 | flex-direction: column;
11 | margin-top: 30px;
12 |
13 | input {
14 | background: rgba(0, 0, 0, 0.15);
15 | border-radius: 4px;
16 | height: 44px;
17 | padding: 0 15px;
18 | color: #fff;
19 | margin: 0 0 10px;
20 |
21 | &::placeholder {
22 | color: #bbb;
23 | }
24 | }
25 |
26 | span {
27 | background: #f64c75;
28 | color: #6d2335;
29 | padding: 4px 8px;
30 | margin: -12px 0 10px;
31 | border-radius: 0 0 4px 4px;
32 | font-weight: bold;
33 | }
34 |
35 | hr {
36 | height: 1px;
37 | background: rgba(255, 255, 255, 0.2);
38 | margin: 10px 0 20px;
39 | }
40 |
41 | button {
42 | background: #3b9eff;
43 | margin: 5px 0 0;
44 | padding: 12px;
45 | height: 44px;
46 | font-weight: bold;
47 | border-radius: 4px;
48 | color: #15385d;
49 | transition: background 150ms ease-in-out;
50 |
51 | &:hover {
52 | background: ${lighten(0.03, '#3b9eff')};
53 | }
54 |
55 | &:active {
56 | background: ${darken(0.05, '#3b9eff')};
57 | }
58 | }
59 | }
60 |
61 | > button {
62 | width: 100%;
63 | background: #f64c75;
64 | margin: 15px 0 0;
65 | padding: 12px;
66 | height: 44px;
67 | font-weight: bold;
68 | border-radius: 4px;
69 | color: #5d313b;
70 | transition: background 150ms ease-in-out;
71 |
72 | &:hover {
73 | background: ${lighten(0.02, '#f64c75')};
74 | }
75 |
76 | &:active {
77 | background: ${darken(0.05, '#f64c75')};
78 | }
79 | }
80 | `;
81 |
--------------------------------------------------------------------------------
/src/store/modules/auth/sagas.js:
--------------------------------------------------------------------------------
1 | import { takeLatest, call, put, all } from 'redux-saga/effects';
2 | import { toast } from 'react-toastify';
3 |
4 | import api from '@/services/api';
5 | import { logInSuccess, signFailure } from './actions';
6 | import history from '@/services/history';
7 |
8 | export function* logIn({ payload }) {
9 | try {
10 | const { email, password } = payload;
11 |
12 | const response = yield call(api.post, 'sessions', {
13 | email,
14 | password,
15 | });
16 |
17 | const { token, user } = response.data;
18 |
19 | if (!user.provider) {
20 | toast.error('User is not a provider.');
21 | return;
22 | }
23 |
24 | api.defaults.headers.Authorization = `Bearer ${token} `;
25 |
26 | yield put(logInSuccess(token, user));
27 |
28 | history.push('/dashboard');
29 | } catch (err) {
30 | toast.error('Authentication failed. Please verify your data.');
31 | yield put(signFailure());
32 | }
33 | }
34 |
35 | export function* signUp({ payload }) {
36 | try {
37 | const { name, email, password } = payload;
38 |
39 | yield call(api.post, 'users', {
40 | name,
41 | email,
42 | password,
43 | provider: true,
44 | });
45 |
46 | history.push('/');
47 | } catch (err) {
48 | toast.error('Registration failed. Please verify your data.');
49 | yield put(signFailure());
50 | }
51 | }
52 |
53 | export function setToken({ payload }) {
54 | if (!payload) return;
55 |
56 | const { token } = payload.auth;
57 |
58 | if (token) {
59 | api.defaults.headers.Authorization = `Bearer ${token} `;
60 | }
61 | }
62 |
63 | export function logOut() {
64 | history.push('/');
65 | }
66 |
67 | export default all([
68 | takeLatest('persist/REHYDRATE', setToken),
69 | takeLatest('@auth/LOG_IN_REQUEST', logIn),
70 | takeLatest('@auth/SIGN_UP_REQUEST', signUp),
71 | takeLatest('@auth/LOG_OUT', logOut),
72 | ]);
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gobarber-web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@craco/craco": "^5.2.4",
7 | "@rocketseat/unform": "^1.5.1",
8 | "axios": "^0.19.0",
9 | "date-fns": "^2.0.0-beta.3",
10 | "date-fns-tz": "^1.0.7",
11 | "history": "^4.9.0",
12 | "immer": "^3.1.3",
13 | "polished": "^3.4.1",
14 | "prop-types": "^15.7.2",
15 | "react": "^16.8.6",
16 | "react-dom": "^16.8.6",
17 | "react-icons": "^3.7.0",
18 | "react-perfect-scrollbar": "^1.5.3",
19 | "react-redux": "^7.1.0",
20 | "react-router-dom": "^5.0.1",
21 | "react-scripts": "3.0.1",
22 | "react-toastify": "^5.3.2",
23 | "reactotron-react-js": "^3.3.2",
24 | "reactotron-redux": "^3.1.1",
25 | "reactotron-redux-saga": "^4.2.2",
26 | "redux": "^4.0.4",
27 | "redux-persist": "^5.10.0",
28 | "redux-saga": "^1.0.5",
29 | "styled-components": "^4.3.2",
30 | "yup": "^0.27.0"
31 | },
32 | "scripts": {
33 | "start": "craco start",
34 | "build": "craco build",
35 | "test": "craco test",
36 | "eject": "react-scripts eject"
37 | },
38 | "eslintConfig": {
39 | "extends": "react-app"
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | },
53 | "devDependencies": {
54 | "eslint": "^5.16.0",
55 | "eslint-config-airbnb": "^17.1.1",
56 | "eslint-config-prettier": "^6.0.0",
57 | "eslint-import-resolver-alias": "^1.1.2",
58 | "eslint-plugin-import": "^2.18.2",
59 | "eslint-plugin-jsx-a11y": "^6.2.3",
60 | "eslint-plugin-prettier": "^3.1.0",
61 | "eslint-plugin-react": "^7.14.3",
62 | "eslint-plugin-react-hooks": "^1.6.1",
63 | "prettier": "^1.18.2"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Notifications/Notifications_Styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import PerfectScrollBar from 'react-perfect-scrollbar';
3 | import { lighten } from 'polished';
4 |
5 | export const Container = styled.div`
6 | position: relative;
7 | `;
8 |
9 | export const Badge = styled.button`
10 | background: none;
11 | position: relative;
12 |
13 | ${({ hasUnread }) =>
14 | hasUnread &&
15 | css`
16 | &::after {
17 | position: absolute;
18 | right: 0;
19 | top: 0;
20 | width: 8px;
21 | height: 8px;
22 | background: #ff892e;
23 | content: '';
24 | border-radius: 50%;
25 | }
26 | `}
27 | `;
28 |
29 | export const NotificationList = styled.div`
30 | position: absolute;
31 | width: 260px;
32 | right: -28px;
33 | top: calc(100% + 30px);
34 | background: rgba(0, 0, 0, 0.6);
35 | border-radius: 4px;
36 | padding: 15px 5px;
37 | display: ${({ visible }) => (visible ? 'block' : 'none')};
38 |
39 | &::before {
40 | content: '';
41 | position: absolute;
42 | right: 20px;
43 | top: -18px;
44 | width: 0;
45 | height: 0;
46 | border-left: 18px solid transparent;
47 | border-right: 18px solid transparent;
48 | border-bottom: 18px solid rgba(0, 0, 0, 0.6);
49 | }
50 | `;
51 |
52 | export const Scroll = styled(PerfectScrollBar)`
53 | max-height: 260px;
54 | padding: 5px 15px;
55 | `;
56 |
57 | export const Notification = styled.div`
58 | color: #fff;
59 |
60 | & + div {
61 | margin-top: 15px;
62 | padding-top: 15px;
63 | border-top: 1px solid rgba(255, 255, 255, 0.1);
64 | }
65 |
66 | p {
67 | font-size: 13px;
68 | line-height: 18px;
69 | }
70 |
71 | time {
72 | font-size: 12px;
73 | opacity: 0.6;
74 | display: block;
75 | margin: 2px 0 0;
76 | }
77 |
78 | button {
79 | font-size: 12px;
80 | background: none;
81 | color: ${lighten(0.2, '#7159c1')};
82 | }
83 |
84 | ${({ unread }) =>
85 | unread &&
86 | css`
87 | &::after {
88 | display: inline-block;
89 | width: 7px;
90 | height: 7px;
91 | background: #ff892e;
92 | content: '';
93 | border-radius: 50%;
94 | margin-left: 8px;
95 | }
96 | `}
97 | `;
98 |
--------------------------------------------------------------------------------
/src/assets/img/gobarber_logo_purple.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/Notifications/Notifications.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from 'react';
2 | import { MdNotifications } from 'react-icons/md';
3 | import { parseISO, formatDistance } from 'date-fns';
4 | import { enUS } from 'date-fns/locale';
5 |
6 | import api from '@/services/api';
7 |
8 | import { Container, Badge, NotificationList, Notification, Scroll } from './Notifications_Styles';
9 |
10 | export default function Notifications() {
11 | const [visible, setVisible] = useState(false);
12 | const [notifications, setNotifications] = useState([]);
13 |
14 | const hasUnread = useMemo(() => !!notifications.find(notification => notification.read === false), [notifications]);
15 |
16 | useEffect(() => {
17 | async function loadNotifications() {
18 | const response = await api.get('notifications');
19 |
20 | const data = response.data.map(notification => ({
21 | ...notification,
22 | timeDistance: formatDistance(parseISO(notification.createdAt), new Date(), {
23 | addSuffix: true,
24 | locale: enUS,
25 | }),
26 | }));
27 |
28 | setNotifications(data);
29 | }
30 | loadNotifications();
31 | }, []);
32 |
33 | function handleToggleVisible() {
34 | setVisible(!visible);
35 | }
36 |
37 | async function handleMarkAsRead(id) {
38 | await api.put(`notifications/${id}`);
39 |
40 | setNotifications(
41 | notifications.map(notification => (notification._id === id ? { ...notification, read: true } : notification))
42 | );
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {notifications.map(notification => (
54 |
55 | {notification.content}
56 |
57 | {!notification.read && (
58 |
61 | )}
62 |
63 | ))}
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/src/pages/Dashboard/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useEffect } from 'react';
2 | import { format, subDays, addDays, setHours, setMinutes, setSeconds, isBefore, parseISO } from 'date-fns';
3 | import { enUS } from 'date-fns/locale';
4 | import { utcToZonedTime } from 'date-fns-tz';
5 | import { MdChevronLeft, MdChevronRight } from 'react-icons/md';
6 |
7 | import { isEqual } from 'date-fns/esm';
8 | import api from '@/services/api';
9 |
10 | import { Container, Time } from './Dashboard_Styles';
11 |
12 | const range = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
13 |
14 | export default function Dashboard() {
15 | const [date, setDate] = useState(new Date());
16 | const [schedule, setSchedule] = useState([]);
17 |
18 | const dateFormatted = useMemo(() => format(date, 'MMMM d', { locale: enUS }), [date]);
19 |
20 | useEffect(() => {
21 | async function loadSchedule() {
22 | const response = await api.get('schedule', {
23 | params: { date },
24 | });
25 |
26 | const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
27 |
28 | const data = range.map(hour => {
29 | const checkDate = setSeconds(setMinutes(setHours(date, hour), 0), 0);
30 | const compareDate = utcToZonedTime(checkDate, timezone);
31 |
32 | return {
33 | time: `${hour}h00`,
34 | past: isBefore(compareDate, new Date()),
35 | appointment: response.data.find(a => isEqual(parseISO(a.date), compareDate)),
36 | };
37 | });
38 |
39 | setSchedule(data);
40 | }
41 | loadSchedule();
42 | }, [date]);
43 |
44 | function handlePrevDay() {
45 | setDate(subDays(date, 1));
46 | }
47 |
48 | function handleNextDay() {
49 | setDate(addDays(date, 1));
50 | }
51 |
52 | return (
53 |
54 |
55 |
58 | {dateFormatted}
59 |
62 |
63 |
64 |
65 | {schedule.map(time => (
66 |
70 | ))}
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | GoBarber Web
5 |
6 |
7 |
8 | A barber scheduling app that shows to the barber his agenda for the day.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Technologies |
33 | How To Use |
34 | License
35 |
36 |
37 | 
38 | 
39 | 
40 | 
41 |
42 | ## :rocket: Technologies
43 |
44 | This project was developed at the [RocketSeat GoStack Bootcamp](https://rocketseat.com.br/bootcamp) with the following technologies:
45 |
46 | - [ReactJS](https://reactjs.org/)
47 | - [Create React App Configuration Override](https://github.com/sharegate/craco)
48 | - [Redux](https://redux.js.org/)
49 | - [Redux-Saga](https://redux-saga.js.org/)
50 | - [React Router v4](https://github.com/ReactTraining/react-router)
51 | - [styled-components](https://www.styled-components.com/)
52 | - [Axios](https://github.com/axios/axios)
53 | - [History](https://www.npmjs.com/package/history)
54 | - [Immer](https://github.com/immerjs/immer)
55 | - [Polished](https://polished.js.org/)
56 | - [React-Toastify](https://fkhadra.github.io/react-toastify/)
57 | - [React-Icons](http://react-icons.github.io/react-icons/)
58 | - [react-perfect-scrollbar](https://github.com/OpusCapita/react-perfect-scrollbar)
59 | - [Unform](https://github.com/Rocketseat/unform)
60 | - [Yup](https://www.npmjs.com/package/yup)
61 | - [date-fns](https://date-fns.org/)
62 | - [Reactotron](https://infinite.red/reactotron)
63 | - [VS Code][vc] with [EditorConfig][vceditconfig] and [ESLint][vceslint]
64 |
65 | ## :information_source: How To Use
66 |
67 | To clone and run this application, you'll need [Git](https://git-scm.com), [Node.js v10.16][nodejs] or higher + [Yarn v1.13][yarn] or higher installed on your computer and the [GoBarber API](https://github.com/lukemorales/gobarber-api). From your command line:
68 |
69 | ```bash
70 | # Clone this repository
71 | $ git clone https://github.com/lukemorales/gobarber-web
72 |
73 | # Go into the repository
74 | $ cd gobarber-web
75 |
76 | # Install dependencies
77 | $ yarn install
78 |
79 | # Run the app
80 | $ yarn start
81 | ```
82 |
83 | ## :memo: License
84 | This project is under the MIT license. See the [LICENSE](https://github.com/lukemorales/gobarber-api/blob/master/LICENSE) for more information.
85 |
86 | ---
87 |
88 | Made with ♥ by Luke Morales :wave: [Get in touch!](https://www.linkedin.com/in/lukemorales/)
89 |
90 | [nodejs]: https://nodejs.org/
91 | [yarn]: https://yarnpkg.com/
92 | [vc]: https://code.visualstudio.com/
93 | [vceditconfig]: https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig
94 | [vceslint]: https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
95 |
--------------------------------------------------------------------------------