Repository: rodrigorgtic/minha-carteira-dashboard
Branch: master
Commit: 85feee3561be
Files: 59
Total size: 70.8 KB
Directory structure:
gitextract_4aoyh11l/
├── .gitignore
├── README.md
├── package.json
├── public/
│ └── index.html
├── src/
│ ├── App.tsx
│ ├── components/
│ │ ├── Aside/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── BarChartBox/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Button/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Content/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── ContentHeader/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── HistoryBox/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── HistoryFinanceCard/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Input/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Layout/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── MainHeader/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── MessageBox/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── PieChartBox/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── SelectInput/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── Toggle/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── WalletBox/
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── hooks/
│ │ ├── auth.tsx
│ │ └── theme.tsx
│ ├── index.tsx
│ ├── pages/
│ │ ├── Dashboard/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ ├── List/
│ │ │ ├── index.tsx
│ │ │ └── styles.ts
│ │ └── SignIn/
│ │ ├── index.tsx
│ │ └── styles.ts
│ ├── react-app-env.d.ts
│ ├── repositories/
│ │ ├── expenses.ts
│ │ └── gains.ts
│ ├── routes/
│ │ ├── app.routes.tsx
│ │ ├── auth.routes.tsx
│ │ └── index.tsx
│ ├── styles/
│ │ ├── GlobalStyles.ts
│ │ ├── styled.d.ts
│ │ └── themes/
│ │ ├── dark.ts
│ │ └── light.ts
│ └── utils/
│ ├── emojis.ts
│ ├── formatCurrency.ts
│ ├── formatDate.ts
│ └── months.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: README.md
================================================
Dashboard desenvolvido em **ReactJs** com **TypeScript** inteiramente componentizado com **componentes puros**.
Não há banco de dados. Os dados são carregados de 2 arquivos que contém arrays simulando os repositórios de dados.
### Layout & Componentes Responsivos
- [x] Link do prototipo desenvolvido no [**Figma**](https://www.figma.com/file/nOGmUkhcINJt6nd57R4ENu/Untitled?node-id=0%3A1).
### Layout & Componentes Responsivos
- [x] Para os gráficos, foi utilizada a bibliteca [**Recharts**](http://recharts.org/en-US) que é opensource.
- [x] Para efeito de número crescendo eu utilizei o [**React CountUp**](https://www.npmjs.com/package/react-countup).
Rodrigo Gonçalves Santana - 2020
================================================
FILE: package.json
================================================
{
"name": "minha-carteira",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/recharts": "^1.8.14",
"react": "^16.13.1",
"react-countup": "^4.3.3",
"react-dom": "^16.13.1",
"react-icons": "^3.10.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-switch": "^5.0.1",
"recharts": "^1.8.5",
"styled-components": "^5.1.1",
"typescript": "~3.7.2",
"uuidv4": "^6.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.1"
}
}
================================================
FILE: public/index.html
================================================
Minha Carteira
You need to enable JavaScript to run this app.
================================================
FILE: src/App.tsx
================================================
import React from 'react';
import { ThemeProvider } from 'styled-components';
import GlobalStyles from './styles/GlobalStyles';
import { useTheme } from './hooks/theme';
import Routes from './routes';
const App: React.FC = () => {
const {theme} = useTheme();
return (
);
}
export default App;
================================================
FILE: src/components/Aside/index.tsx
================================================
import React, {useState} from 'react';
import Toggle from '../Toggle';
import {
MdDashboard,
MdArrowDownward,
MdArrowUpward,
MdExitToApp,
MdClose,
MdMenu,
} from 'react-icons/md';
import logoImg from '../../assets/logo.svg';
import { useAuth } from '../../hooks/auth';
import { useTheme } from '../../hooks/theme';
import {
Container,
Header,
LogImg,
Title,
MenuContainer,
MenuItemLink,
MenuItemButton,
ToggleMenu,
ThemeToggleFooter,
} from './styles';
const Aside: React.FC = () => {
const { signOut } = useAuth();
const { toggleTheme, theme } = useTheme();
const [toggleMenuIsOpened, setToggleMenuIsOpened ] = useState(false);
const [darkTheme, setDarkTheme] = useState(() => theme.title === 'dark' ? true : false);
const handleToggleMenu = () => {
setToggleMenuIsOpened(!toggleMenuIsOpened);
}
const handleChangeTheme = () => {
setDarkTheme(!darkTheme);
toggleTheme();
}
return (
{ toggleMenuIsOpened ? : }
Minha Carteira
Dashboard
Entradas
Saídas
Sair
);
}
export default Aside;
================================================
FILE: src/components/Aside/styles.ts
================================================
import styled, { css } from 'styled-components';
interface IContainerProps {
menuIsOpen: boolean;
}
interface IThemeToggleFooterProps {
menuIsOpen: boolean;
}
export const Container = styled.div`
grid-area: AS;
background-color: ${props => props.theme.colors.secondary};
padding-left: 20px;
border-right: 1px solid ${props => props.theme.colors.gray};
position: relative;
@media(max-width: 600px){
padding-left: 20px;
position: fixed;
z-index: 2;
width: 170px;
height: ${props => props.menuIsOpen ? '100vh' : '70px'};
overflow: hidden;
${props => !props.menuIsOpen && css`
border: none;
border-bottom: 1px solid ${props => props.theme.colors.gray};
`};
}
`;
export const Header = styled.header`
height: 70px;
display: flex;
align-items: center;
`;
export const LogImg = styled.img`
height: 40px;
width: 40px;
@media(max-width: 600px){
display: none;
}
`;
export const Title = styled.h3`
color: ${props => props.theme.colors.white};
margin-left: 10px;
@media(max-width: 600px){
display: none;
}
`;
export const MenuContainer = styled.nav`
display: flex;
flex-direction: column;
margin-top: 50px;
`;
export const MenuItemLink = styled.a`
color: ${props => props.theme.colors.info};
text-decoration: none;
margin: 7px 0;
display: flex;
align-items: center;
transition: opacity .3s;
&:hover {
opacity: .7;
}
> svg {
font-size: 18px;
margin-right: 5px;
}
`;
export const MenuItemButton = styled.button`
font-size: 16px;
color: ${props => props.theme.colors.info};
border: none;
background: none;
margin: 7px 0;
display: flex;
align-items: center;
transition: opacity .3s;
&:hover {
opacity: .7;
}
> svg {
font-size: 18px;
margin-right: 5px;
}
`;
export const ToggleMenu = styled.button`
width: 40px;
height: 40px;
border-radius: 5px;
font-size: 22px;
background-color: ${props => props.theme.colors.warning};
color: ${props => props.theme.colors.white};
transition: opacity .3s;
&:hover{
opacity: 0.7;
}
display: none;
@media(max-width: 600px){
display: flex;
justify-content: center;
align-items: center;
}
`;
export const ThemeToggleFooter = styled.footer`
display: none;
position: absolute;
bottom: 30px;
@media(max-width: 470px){
display: ${props => props.menuIsOpen ? 'flex' : 'none'};
}
`;
================================================
FILE: src/components/BarChartBox/index.tsx
================================================
import React from 'react';
import {
ResponsiveContainer,
BarChart,
Bar,
Cell,
Tooltip,
} from 'recharts';
import formatCurrency from '../../utils/formatCurrency';
import {
Container,
SideLeft,
SideRight,
LegendContainer,
Legend,
} from './styles';
interface IBarChartProps {
title: string;
data: {
name: string;
amount: number;
percent: number;
color: string
}[],
}
const BarChartBox: React.FC = ({ title, data }) => (
{title}
{
data.map((indicator) => (
{indicator.percent}%
{indicator.name}
))
}
{
data.map((indicator) => (
|
))
}
formatCurrency(Number(value))}
/>
);
export default BarChartBox;
================================================
FILE: src/components/BarChartBox/styles.ts
================================================
import styled, { keyframes } from 'styled-components';
interface ILegendProps {
color: string;
}
const animate = keyframes`
0% {
transform: translateX(100px);
opacity: 0;
}
50%{
opacity: .3;
}
100%{
transform: translateX(0px);
opacity: 1;
}
`;
export const Container = styled.div`
width: 48%;
min-height: 260px;
margin: 10px 0;
background-color: ${props => props.theme.colors.tertiary};
color: ${props => props.theme.colors.white};
border-radius: 7px;
display: flex;
animation: ${animate} .5s;
@media(max-width: 1200px){
display: flex;
flex-direction: column;
width: 100%;
height: auto;
}
`;
export const SideLeft = styled.aside`
flex: 1;
padding: 30px 20px;
> h2 {
padding-left: 16px;
margin-bottom: 10px;
}
`;
export const LegendContainer = styled.ul`
list-style: none;
height: 175px;
padding-right: 15px;
overflow-y: scroll;
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background-color: ${props => props.theme.colors.secondary};
border-radius: 10px;
}
::-webkit-scrollbar-track {
background-color: ${props => props.theme.colors.tertiary};
}
@media(max-width: 1200px){
display: flex;
height: auto;
}
`;
export const Legend = styled.li`
display: flex;
align-items: center;
margin-bottom: 7px;
padding-left: 16px;
> div {
background-color: ${props => props.color};
width: 40px;
height: 40px;
border-radius: 5px;
font-size: 14px;
line-height: 40px;
text-align: center;
}
> span {
margin-left: 5px;
}
@media(max-width: 1200px){
> div {
width: 30px;
height: 30px;
font-size: 10px;
line-height: 30px;
}
}
`;
export const SideRight = styled.main`
flex: 1;
min-height: 150px;
display: flex;
justify-content: center;
padding-top: 35px;
`;
================================================
FILE: src/components/Button/index.tsx
================================================
import React, { ButtonHTMLAttributes } from 'react';
import { Container } from './styles'
type IButtonProps = ButtonHTMLAttributes;
const Button: React.FC = ({children, ...rest }) => (
{children}
);
export default Button;
================================================
FILE: src/components/Button/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.button`
width: 100%;
margin: 7px 0;
padding: 10px;
border-radius: 5px;
font-weight: bold;
color: ${props => props.theme.colors.white};
background-color: ${props => props.theme.colors.warning};
transition: opacity .3s;
&:hover{
opacity: .7;
}
`;
================================================
FILE: src/components/Content/index.tsx
================================================
import React from 'react';
import { Container } from './styles';
const Content: React.FC = ({ children }) => (
{children}
);
export default Content;
================================================
FILE: src/components/Content/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.div`
grid-area: CT;
color: ${props => props.theme.colors.white};
background-color: ${props => props.theme.colors.primary};
padding: 25px;
height: calc(100vh - 70px);
overflow-y: scroll;
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background-color: ${props => props.theme.colors.secondary};
border-radius: 10px;
}
::-webkit-scrollbar-track {
background-color: ${props => props.theme.colors.tertiary};
}
`;
================================================
FILE: src/components/ContentHeader/index.tsx
================================================
import React from 'react';
import {
Container,
TitleContainer,
Controllers
} from './styles';
interface IContentHeaderProps {
title: string;
lineColor: string;
children: React.ReactNode;
}
const ContentHeader: React.FC = ({
title, lineColor, children
}) => (
{title}
{children}
);
export default ContentHeader;
================================================
FILE: src/components/ContentHeader/styles.ts
================================================
import styled from 'styled-components';
interface ITitleContainerProps {
lineColor: string;
}
export const Container = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 25px;
@media(max-width: 320px){
flex-direction: column;
}
`;
export const TitleContainer = styled.div`
> h1 {
color: ${props => props.theme.colors.white};
&::after {
content: '';
display: block;
width: 55px;
border-bottom: 10px solid ${props => props.lineColor};
}
}
@media(max-width: 420px){
> h1 {
font-size: 22px;
&::after {
content: '';
display: block;
width: 55px;
border-bottom: 5px solid ${props => props.lineColor};
}
}
}
`;
export const Controllers = styled.div`
display: flex;
@media(max-width: 320px){
width: 100%;
justify-content: space-around;
margin-top: 20px;
}
`;
================================================
FILE: src/components/HistoryBox/index.tsx
================================================
import React from 'react';
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
CartesianGrid,
Tooltip,
} from 'recharts';
import formatCurrency from '../../utils/formatCurrency';
import {
Container,
ChartContainer,
Header,
LegendContainer,
Legend,
} from './styles';
interface IHistoryBoxProps {
data: {
month: string;
amountEntry: number;
amountOutput: number;
}[],
lineColorAmountEntry: string;
lineColorAmountOutput: string;
}
const HistoryBox: React.FC = ({
data, lineColorAmountEntry, lineColorAmountOutput
}) => (
Histórico de saldo
Entradas
Saídas
formatCurrency(Number(value))} />
)
export default HistoryBox;
================================================
FILE: src/components/HistoryBox/styles.ts
================================================
import styled, {keyframes} from 'styled-components';
interface ILegendProps {
color: string;
}
const animate = keyframes`
0% {
transform: translateX(-100px);
opacity: 0;
}
50%{
opacity: .3;
}
100%{
transform: translateX(0px);
opacity: 1;
}
`;
export const Container = styled.div`
width: 100%;
display: flex;
flex-direction: column;
background-color: ${props => props.theme.colors.tertiary};
color: ${props => props.theme.colors.white};
margin: 10px 0;
padding: 30px 20px;
border-radius: 7px;
animation: ${animate} .5s;
`;
export const ChartContainer = styled.div`
flex: 1;
height: 260px;
`;
export const Header = styled.header`
width: 100%;
display: flex;
justify-content: space-between;
> h2 {
margin-bottom: 20px;
padding-left: 16px;
}
@media(max-width: 1200px){
flex-direction: column;
}
`;
export const LegendContainer = styled.ul`
list-style: none;
display: flex;
padding-right: 16px;
`;
export const Legend = styled.li`
display: flex;
align-items: center;
margin-bottom: 7px;
margin-left: 16px;
> div {
background-color: ${props => props.color};
width: 40px;
height: 40px;
border-radius: 5px;
font-size: 14px;
line-height: 40px;
text-align: center;
}
> span {
margin-left: 5px;
}
@media(max-width: 1280px){
> div {
width: 30px;
height: 30px;
}
}
`;
================================================
FILE: src/components/HistoryFinanceCard/index.tsx
================================================
import React from 'react';
import { Container, Tag } from './styles';
interface IHistoryFinanceCardProps {
tagColor: string;
title: string;
subtitle: string;
amount: string;
}
const HistoryFinanceCard: React.FC = ({
tagColor,
title,
subtitle,
amount
}) => (
{title}
{subtitle}
{amount}
);
export default HistoryFinanceCard;
================================================
FILE: src/components/HistoryFinanceCard/styles.ts
================================================
import styled, { keyframes } from 'styled-components';
interface ITagProps {
color: string;
}
const animate = keyframes`
0% {
transform: translateX(-100px);
opacity: 0;
}
50%{
opacity: .3;
}
100%{
transform: translateX(0px);
opacity: 1;
}
`;
export const Container = styled.li`
background-color: ${props => props.theme.colors.tertiary};
list-style: none;
border-radius: 10px;
margin: 10px 0;
padding: 12px 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all .3s;
position: relative;
animation: ${animate} .5s ease;
&:hover {
opacity: .7;
transform: translateX(10px);
}
> div {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 10px;
}
> div span {
font-size: 22px;
font-weight: 500;
}
`;
export const Tag = styled.div`
width: 13px;
height: 60%;
background-color: ${props => props.color};
position: absolute;
left: 0;
`;
================================================
FILE: src/components/Input/index.tsx
================================================
import React, { InputHTMLAttributes } from 'react';
import { Container } from './styles'
type IInputProps = InputHTMLAttributes;
const Input: React.FC = ({ ...rest }) => (
);
export default Input;
================================================
FILE: src/components/Input/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.input`
width: 100%;
margin: 7px 0;
padding: 10px;
border-radius: 5px;
`;
================================================
FILE: src/components/Layout/index.tsx
================================================
import React from 'react';
import { Grid } from './styles';
import MainHeader from '../MainHeader';
import Aside from '../Aside';
import Content from '../Content';
const Layout: React.FC = ({ children }) => (
{ children }
);
export default Layout;
================================================
FILE: src/components/Layout/styles.ts
================================================
import styled from 'styled-components';
/**
* Layout
* MH = Main Header
* AS = Aside
* CT = Content
*/
export const Grid = styled.div`
display: grid;
grid-template-columns: 250px auto;
grid-template-rows: 70px auto;
grid-template-areas:
'AS MH'
'AS CT';
height: 100vh;
min-width: 315px;
@media(max-width: 600px){
grid-template-columns: 100%;
grid-template-rows: 70px auto;
grid-template-areas:
'MH'
'CT';
}
`;
================================================
FILE: src/components/MainHeader/index.tsx
================================================
import React, { useMemo, useState } from 'react';
import Toggle from '../Toggle';
import emojis from '../../utils/emojis';
import { useTheme } from '../../hooks/theme';
import {
Container,
Profile,
Welcome,
UserName,
} from './styles';
const MainHeader: React.FC = () => {
const { toggleTheme, theme } = useTheme();
const [darkTheme, setDarkTheme] = useState(() => theme.title === 'dark' ? true : false);
const handleChangeTheme = () => {
setDarkTheme(!darkTheme);
toggleTheme();
}
const emoji = useMemo(() => {
const indice = Math.floor(Math.random() * emojis.length);
return emojis[indice];
},[]);
return (
Olá, {emoji}
Rodrigo Gonçalves
);
}
export default MainHeader;
================================================
FILE: src/components/MainHeader/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.div`
grid-area: MH;
background-color: ${props => props.theme.colors.secondary};
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid ${props => props.theme.colors.gray};
`;
export const Profile = styled.div`
color: ${props => props.theme.colors.white};
`;
export const Welcome = styled.h3``;
export const UserName = styled.span``;
================================================
FILE: src/components/MessageBox/index.tsx
================================================
import React from 'react';
import { Container } from './styles';
interface IMessageBoxProps {
title: string;
description: string;
footerText: string;
icon: string;
}
const MessageBox: React.FC = ({
title,
description,
footerText,
icon,
}) => (
{title}
{description}
);
export default MessageBox;
================================================
FILE: src/components/MessageBox/styles.ts
================================================
import styled, {keyframes} from 'styled-components';
const animate = keyframes`
0% {
transform: translateX(-100px);
opacity: 0;
}
50%{
opacity: .3;
}
100%{
transform: translateX(0px);
opacity: 1;
}
`;
export const Container = styled.div`
width: 48%;
height: 260px;
background-color: ${props => props.theme.colors.tertiary};
color: ${props => props.theme.colors.white};
border-radius: 7px;
margin: 10px 0;
padding: 30px 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
animation: ${animate} .5s;
> header img {
width: 35px;
margin-left: 7px;
}
> header p {
font-size: 18px;
}
@media(max-width: 770px){
width: 100%;
> header h1 {
font-size: 24px;
img {
height: 20px;
width: 20px;
}
}
> header p, > footer span {
font-size: 14px;
}
}
@media(max-width: 420px){
width: 100%;
height: auto;
> header p {
margin-bottom: 15px;
}
}
`;
================================================
FILE: src/components/PieChartBox/index.tsx
================================================
import React from 'react';
import {
PieChart,
Pie,
Cell,
ResponsiveContainer
} from 'recharts';
import {
Container,
SideLeft,
LegendContainer,
Legend,
SideRight,
} from './styles';
interface IPieChartProps {
data: {
name: string;
value: number;
percent: number;
color: string;
}[];
}
const PieChartBox: React.FC = ({ data }) => (
Relação
{
data.map((indicator) => (
{indicator.percent}%
{indicator.name}
))
}
{
data.map((indicator) => (
|
))
}
);
export default PieChartBox;
================================================
FILE: src/components/PieChartBox/styles.ts
================================================
import styled, { keyframes } from 'styled-components';
interface ILegendProps {
color: string;
}
const animate = keyframes`
0% {
transform: translateX(100px);
opacity: 0;
}
50%{
opacity: .3;
}
100%{
transform: translateX(0px);
opacity: 1;
}
`;
export const Container = styled.div`
width: 48%;
height: 260px;
margin: 10px 0;
background-color: ${props => props.theme.colors.tertiary};
color: ${props => props.theme.colors.white};
border-radius: 7px;
display: flex;
animation: ${animate} .5s;
@media(max-width: 770px){
display: flex;
width: 100%;
}
`;
export const SideLeft = styled.aside`
padding: 30px 20px;
> h2 {
margin-bottom: 20px;
}
@media(max-width: 1345px){
padding: 0 15px 5px;
margin-bottom: 7px;
> h2 {
margin-top: 15px;
margin-bottom: 7px;
}
}
@media(max-width: 420px){
padding: 15px;
margin-bottom: 7px;
}
`;
export const LegendContainer = styled.ul`
list-style: none;
height: 175px;
padding-right: 15px;
overflow-y: scroll;
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background-color: ${props => props.theme.colors.secondary};
border-radius: 10px;
}
::-webkit-scrollbar-track {
background-color: ${props => props.theme.colors.tertiary};
}
@media(max-width: 1345px){
display: flex;
flex-direction: column;
}
`;
export const Legend = styled.li`
display: flex;
align-items: center;
margin-bottom: 7px;
> div {
background-color: ${props => props.color};
width: 40px;
height: 40px;
border-radius: 5px;
font-size: 14px;
line-height: 40px;
text-align: center;
}
> span {
margin-left: 5px;
}
@media(max-width: 145px){
font-size: 14px;
margin: 3px 0;
> div {
height: 35px;
width: 35px;
line-height: 35px;
}
> span {
margin-left: 7px;
}
}
`;
export const SideRight = styled.main`
display: flex;
flex: 1;
justify-content: center;
@media(max-width: 1345px){
height: 100%;
}
`;
================================================
FILE: src/components/SelectInput/index.tsx
================================================
import React from 'react';
import { Container } from './styles';
interface ISelectInputProps {
options: {
value: string | number;
label: string | number;
}[],
onChange(event: React.ChangeEvent): void | undefined;
defaultValue?: string | number;
}
const SelectInput: React.FC = ({
options,
onChange,
defaultValue
}) => (
{
options.map(option => (
{option.label}
))
}
);
export default SelectInput;
================================================
FILE: src/components/SelectInput/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.div`
> select {
padding: 7px 10px;
border-radius: 5px;
margin-left: 7px;
}
`;
================================================
FILE: src/components/Toggle/index.tsx
================================================
import React from 'react';
import {
Container,
ToggleLabel,
ToggleSelector
} from './styles';
interface IToggleProps {
labelLeft: string;
labelRight: string;
checked: boolean;
onChange(): void;
}
const Toggle: React.FC = ({
labelLeft,
labelRight,
checked,
onChange
}) => (
{labelLeft}
{labelRight}
)
export default Toggle;
================================================
FILE: src/components/Toggle/styles.ts
================================================
import styled from 'styled-components';
import Switch, { ReactSwitchProps } from 'react-switch';
export const Container = styled.div`
display: flex;
align-items: center;
`;
export const ToggleLabel = styled.span`
color: ${props => props.theme.colors.white};
`;
export const ToggleSelector = styled(Switch).attrs(
({ theme }) => ({
onColor: theme.colors.info,
offColor: theme.colors.warning
}))`
margin: 0 7px;
`;
================================================
FILE: src/components/WalletBox/index.tsx
================================================
import React, { useMemo } from 'react';
import CountUp from 'react-countup';
import dolarImg from '../../assets/dolar.svg';
import arrowUpImg from '../../assets/arrow-up.svg';
import arrowDownImg from '../../assets/arrow-down.svg';
import { Container } from './styles';
interface IWalletBoxProps {
title: string;
amount: number;
footerlabel: string;
icon: 'dolar' | 'arrowUp' | 'arrowDown';
color: string;
}
const WalletBox: React.FC = ({
title,
amount,
footerlabel,
icon,
color
}) => {
const iconSelected = useMemo(() => {
switch (icon) {
case 'dolar':
return dolarImg;
case 'arrowUp':
return arrowUpImg;
case 'arrowDown':
return arrowDownImg;
default:
return undefined;
}
},[icon]);
return (
{title}
R$
{footerlabel}
);
}
export default WalletBox;
================================================
FILE: src/components/WalletBox/styles.ts
================================================
import styled, {keyframes} from 'styled-components';
interface IContainerProps {
color: string;
}
const animate = keyframes`
0%{
transform: translateX(100px);
opacity: 0;
}
50%{
opacity: .3;
}
100%{
transform: translateX(0px);
opacity: 1;
}
`;
export const Container = styled.div`
width: 32%;
height: 150px;
margin: 10px 0;
background-color: ${props => props.color};
color: ${props => props.theme.colors.white};
border-radius: 7px;
padding: 10px 20px;
position: relative;
overflow: hidden;
animation: ${animate} .5s;
> img {
height: 110%;
position: absolute;
top: -10px;
right: -30px;
opacity: .3;
}
> span {
font-size: 18px;
font-weight: 500;
}
> small {
font-size: 12px;
position: absolute;
bottom: 10px;
}
@media(max-width: 770px){
> span {
font-size: 14px;
}
> h1 {
word-wrap: break-word;
font-size: 22px;
strong {
display: inline-block;
width: 100%;
font-size: 16px;
}
}
}
@media(max-width: 420px){
width: 100%;
> h1 {
display: flex;
strong {
position: initial;
width: auto;
font-size: 22px;
}
strong:after {
display: inline-block;
content: '';
width: 1px;
}
}
}
`;
================================================
FILE: src/hooks/auth.tsx
================================================
import React, { createContext, useState, useContext } from 'react';
interface IAuthContext {
logged: boolean;
signIn(email: string, password: string): void;
signOut(): void;
}
const AuthContext = createContext({} as IAuthContext);
const AuthProvider: React.FC = ({ children }) => {
const [logged, setLogged] = useState(() => {
const isLogged = localStorage.getItem('@minha-carteira:logged');
return !!isLogged;
});
const signIn = (email: string, password: string) => {
if(email === 'rodrigo@email.com' && password === '123'){
localStorage.setItem('@minha-carteira:logged', 'true');
setLogged(true);
}else{
alert('Senha ou usuário inválidos!');
}
}
const signOut = () => {
localStorage.removeItem('@minha-carteira:logged');
setLogged(false);
}
return (
{children}
);
}
function useAuth(): IAuthContext {
const context = useContext(AuthContext);
return context;
}
export { AuthProvider, useAuth };
================================================
FILE: src/hooks/theme.tsx
================================================
import React, { createContext, useState, useContext } from 'react';
import dark from '../styles/themes/dark';
import light from '../styles/themes/light';
interface IThemeContext {
toggleTheme(): void;
theme: ITheme;
}
interface ITheme {
title: string;
colors: {
primary: string;
secondary: string;
tertiary: string;
white: string;
black: string;
gray: string;
success: string;
info: string;
warning: string;
}
}
const ThemeContext = createContext({} as IThemeContext);
const ThemeProvider: React.FC = ({ children }) => {
const [theme, setTheme] = useState(() => {
const themeSaved = localStorage.getItem('@minha-carteira:theme');
if(themeSaved) {
return JSON.parse(themeSaved);
}else{
return dark;
}
});
const toggleTheme = () => {
if(theme.title === 'dark'){
setTheme(light);
localStorage.setItem('@minha-carteira:theme', JSON.stringify(light));
}else{
setTheme(dark);
localStorage.setItem('@minha-carteira:theme', JSON.stringify(dark));
}
};
return (
{children}
)
}
function useTheme(): IThemeContext {
const context = useContext(ThemeContext);
return context;
}
export { ThemeProvider, useTheme };
================================================
FILE: src/index.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import { ThemeProvider } from './hooks/theme';
import { AuthProvider } from './hooks/auth';
import App from './App';
ReactDOM.render(
,
document.getElementById('root')
);
================================================
FILE: src/pages/Dashboard/index.tsx
================================================
import React, { useState, useMemo, useCallback } from 'react';
import ContentHeader from '../../components/ContentHeader';
import SelectInput from '../../components/SelectInput';
import WalletBox from '../../components/WalletBox';
import MessageBox from '../../components/MessageBox';
import PieChartBox from '../../components/PieChartBox';
import HistoryBox from '../../components/HistoryBox';
import BarChartBox from '../../components/BarChartBox'
import expenses from '../../repositories/expenses';
import gains from '../../repositories/gains';
import listOfMonths from '../../utils/months';
import happyImg from '../../assets/happy.svg';
import sadImg from '../../assets/sad.svg';
import grinningImg from '../../assets/grinning.svg';
import opsImg from '../../assets/ops.svg';
import {
Container,
Content,
} from './styles';
const Dashboard: React.FC = () => {
const [monthSelected, setMonthSelected] = useState(new Date().getMonth() + 1);
const [yearSelected, setYearSelected] = useState(new Date().getFullYear());
const years = useMemo(() => {
let uniqueYears: number[] = [];
[...expenses, ...gains].forEach(item => {
const date = new Date(item.date);
const year = date.getFullYear();
if(!uniqueYears.includes(year)){
uniqueYears.push(year)
}
});
return uniqueYears.map(year => {
return {
value: year,
label: year,
}
});
},[]);
const months = useMemo(() => {
return listOfMonths.map((month, index) => {
return {
value: index + 1,
label: month,
}
});
},[]);
const totalExpenses = useMemo(() => {
let total: number = 0;
expenses.forEach(item => {
const date = new Date(item.date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
if(month === monthSelected && year === yearSelected){
try{
total += Number(item.amount)
}catch{
throw new Error('Invalid amount! Amount must be number.')
}
}
});
return total;
},[monthSelected, yearSelected]);
const totalGains = useMemo(() => {
let total: number = 0;
gains.forEach(item => {
const date = new Date(item.date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
if(month === monthSelected && year === yearSelected){
try{
total += Number(item.amount)
}catch{
throw new Error('Invalid amount! Amount must be number.')
}
}
});
return total;
},[monthSelected, yearSelected]);
const totalBalance = useMemo(() => {
return totalGains - totalExpenses;
},[totalGains, totalExpenses]);
const message = useMemo(() => {
if(totalBalance < 0){
return {
title: "Que triste!",
description: "Neste mês, você gastou mais do que deveria.",
footerText: "Verifique seus gastos e tente cortar algumas coisas desnecessárias.",
icon: sadImg
}
}
else if(totalGains === 0 && totalExpenses === 0){
return {
title: "Op's!",
description: "Neste mês, não há registros de entradas ou saídas.",
footerText: "Parece que você não fez nenhum registro no mês e ano selecionado.",
icon: opsImg
}
}
else if(totalBalance === 0){
return {
title: "Ufaa!",
description: "Neste mês, você gastou exatamente o que ganhou.",
footerText: "Tenha cuidado. No próximo tente poupar o seu dinheiro.",
icon: grinningImg
}
}
else{
return {
title: "Muito bem!",
description: "Sua carteira está positiva!",
footerText: "Continue assim. Considere investir o seu saldo.",
icon: happyImg
}
}
},[totalBalance, totalGains, totalExpenses]);
const relationExpensesVersusGains = useMemo(() => {
const total = totalGains + totalExpenses;
const percentGains = Number(((totalGains / total) * 100).toFixed(1));
const percentExpenses = Number(((totalExpenses / total) * 100).toFixed(1));
const data = [
{
name: "Entradas",
value: totalGains,
percent: percentGains ? percentGains : 0,
color: '#E44C4E'
},
{
name: "Saídas",
value: totalExpenses,
percent: percentExpenses ? percentExpenses : 0,
color: '#F7931B'
},
];
return data;
},[totalGains, totalExpenses]);
const historyData = useMemo(() => {
return listOfMonths
.map((_, month) => {
let amountEntry = 0;
gains.forEach(gain => {
const date = new Date(gain.date);
const gainMonth = date.getMonth();
const gainYear = date.getFullYear();
if(gainMonth === month && gainYear === yearSelected){
try{
amountEntry += Number(gain.amount);
}catch{
throw new Error('amountEntry is invalid. amountEntry must be valid number.')
}
}
});
let amountOutput = 0;
expenses.forEach(expense => {
const date = new Date(expense.date);
const expenseMonth = date.getMonth();
const expenseYear = date.getFullYear();
if(expenseMonth === month && expenseYear === yearSelected){
try{
amountOutput += Number(expense.amount);
}catch{
throw new Error('amountOutput is invalid. amountOutput must be valid number.')
}
}
});
return {
monthNumber: month,
month: listOfMonths[month].substr(0, 3),
amountEntry,
amountOutput
}
})
.filter(item => {
const currentMonth = new Date().getMonth();
const currentYear = new Date().getFullYear();
return (yearSelected === currentYear && item.monthNumber <= currentMonth) || (yearSelected < currentYear)
});
},[yearSelected]);
const relationExpensevesRecurrentVersusEventual = useMemo(() => {
let amountRecurrent = 0;
let amountEventual = 0;
expenses
.filter((expense) => {
const date = new Date(expense.date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
return month === monthSelected && year === yearSelected;
})
.forEach((expense) => {
if(expense.frequency === 'recorrente'){
return amountRecurrent += Number(expense.amount);
}
if(expense.frequency === 'eventual'){
return amountEventual += Number(expense.amount);
}
});
const total = amountRecurrent + amountEventual;
const percentRecurrent = Number(((amountRecurrent / total) * 100).toFixed(1));
const percentEventual = Number(((amountEventual / total) * 100).toFixed(1));
return [
{
name: 'Recorrentes',
amount: amountRecurrent,
percent: percentRecurrent ? percentRecurrent : 0,
color: "#F7931B"
},
{
name: 'Eventuais',
amount: amountEventual,
percent: percentEventual ? percentEventual : 0,
color: "#E44C4E"
}
];
},[monthSelected, yearSelected]);
const relationGainsRecurrentVersusEventual = useMemo(() => {
let amountRecurrent = 0;
let amountEventual = 0;
gains
.filter((gain) => {
const date = new Date(gain.date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
return month === monthSelected && year === yearSelected;
})
.forEach((gain) => {
if(gain.frequency === 'recorrente'){
return amountRecurrent += Number(gain.amount);
}
if(gain.frequency === 'eventual'){
return amountEventual += Number(gain.amount);
}
});
const total = amountRecurrent + amountEventual;
const percentRecurrent = Number(((amountRecurrent / total) * 100).toFixed(1));
const percentEventual = Number(((amountEventual / total) * 100).toFixed(1));
return [
{
name: 'Recorrentes',
amount: amountRecurrent,
percent: percentRecurrent ? percentRecurrent : 0,
color: "#F7931B"
},
{
name: 'Eventuais',
amount: amountEventual,
percent: percentEventual ? percentEventual : 0,
color: "#E44C4E"
}
];
},[monthSelected, yearSelected]);
const handleMonthSelected = useCallback((month: string) => {
try {
const parseMonth = Number(month);
setMonthSelected(parseMonth);
}
catch{
throw new Error('invalid month value. Is accept 0 - 24.')
}
},[]);
const handleYearSelected = useCallback((year: string) => {
try {
const parseYear = Number(year);
setYearSelected(parseYear);
}
catch{
throw new Error('invalid year value. Is accept integer numbers.')
}
},[]);
return (
handleMonthSelected(e.target.value)}
defaultValue={monthSelected}
/>
handleYearSelected(e.target.value)}
defaultValue={yearSelected}
/>
);
}
export default Dashboard;
================================================
FILE: src/pages/Dashboard/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.div``;
export const Content = styled.div`
display: flex;
justify-content: space-between;
flex-wrap: wrap;
`;
================================================
FILE: src/pages/List/index.tsx
================================================
import React, { useMemo, useState, useEffect } from 'react';
import { uuid } from 'uuidv4';
import ContentHeader from '../../components/ContentHeader';
import SelectInput from '../../components/SelectInput';
import HistoryFinanceCard from '../../components/HistoryFinanceCard';
import gains from '../../repositories/gains';
import expenses from '../../repositories/expenses';
import formatCurrency from '../../utils/formatCurrency';
import formatDate from '../../utils/formatDate';
import listOfMonths from '../../utils/months';
import {
Container,
Content,
Filters
} from './styles';
interface IRouteParams {
match: {
params: {
type: string;
}
}
}
interface IData {
id: string;
description: string;
amountFormatted: string;
frequency: string;
dateFormatted: string;
tagColor: string;
}
const List: React.FC = ({ match }) => {
const [data, setData] = useState([]);
const [monthSelected, setMonthSelected] = useState(new Date().getMonth() + 1);
const [yearSelected, setYearSelected] = useState(new Date().getFullYear());
const [frequencyFilterSelected, setFrequencyFilterSelected] = useState(['recorrente', 'eventual']);
const movimentType = match.params.type;
const pageData = useMemo(() => {
return movimentType === 'entry-balance' ?
{
title: 'Entradas',
lineColor: '#4E41F0',
data: gains
}
:
{
title: 'Saídas',
lineColor: '#E44C4E',
data: expenses
}
},[movimentType]);
const years = useMemo(() => {
let uniqueYears: number[] = [];
const { data } = pageData;
data.forEach(item => {
const date = new Date(item.date);
const year = date.getFullYear();
if(!uniqueYears.includes(year)){
uniqueYears.push(year)
}
});
return uniqueYears.map(year => {
return {
value: year,
label: year,
}
});
},[pageData]);
const months = useMemo(() => {
return listOfMonths.map((month, index) => {
return {
value: index + 1,
label: month,
}
});
},[]);
const handleFrequencyClick = (frequency: string) => {
const alreadySelected = frequencyFilterSelected.findIndex(item => item === frequency);
if(alreadySelected >= 0){
const filtered = frequencyFilterSelected.filter(item => item !== frequency);
setFrequencyFilterSelected(filtered);
}else{
setFrequencyFilterSelected((prev) => [...prev, frequency]);
}
}
const handleMonthSelected = (month: string) => {
try {
const parseMonth = Number(month);
setMonthSelected(parseMonth);
}
catch{
throw new Error('invalid month value. Is accept 0 - 24.')
}
}
const handleYearSelected = (year: string) => {
try {
const parseYear = Number(year);
setYearSelected(parseYear);
}
catch{
throw new Error('invalid year value. Is accept integer numbers.')
}
}
useEffect(() => {
const { data } = pageData;
const filteredData = data.filter(item => {
const date = new Date(item.date);
const month = date.getMonth() + 1;
const year = date.getFullYear();
return month === monthSelected && year === yearSelected && frequencyFilterSelected.includes(item.frequency);
});
const formattedData = filteredData.map(item => {
return {
id: uuid(),
description: item.description,
amountFormatted: formatCurrency(Number(item.amount)),
frequency: item.frequency,
dateFormatted: formatDate(item.date),
tagColor: item.frequency === 'recorrente' ? '#4E41F0' : '#E44C4E',
}
});
setData(formattedData);
},[pageData, monthSelected, yearSelected, data.length, frequencyFilterSelected]);
return (
handleMonthSelected(e.target.value)}
defaultValue={monthSelected}
/>
handleYearSelected(e.target.value)}
defaultValue={yearSelected}
/>
handleFrequencyClick('recorrente')}
>
Recorrentes
handleFrequencyClick('eventual')}
>
Eventuais
{
data.map(item => (
))
}
);
}
export default List;
================================================
FILE: src/pages/List/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.div``;
export const Content = styled.main``;
export const Filters = styled.div`
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 30px;
.tag-filter {
font-size: 18px;
font-weight: 500;
background: none;
color: ${props => props.theme.colors.white};
margin: 0 10px;
opacity: .4;
transition: opacity .3s;
&:hover {
opacity: .7;
}
}
.tag-filter-recurrent::after {
content: '';
display: block;
width: 55px;
margin: 0 auto;
border-bottom: 10px solid ${props => props.theme.colors.success};
}
.tag-filter-eventual::after {
content: '';
display: block;
width: 55px;
margin: 0 auto;
border-bottom: 10px solid ${props => props.theme.colors.warning};
}
.tag-actived {
opacity: 1;
}
`;
================================================
FILE: src/pages/SignIn/index.tsx
================================================
import React, {useState} from 'react';
import logoImg from '../../assets/logo.svg';
import Input from '../../components/Input';
import Button from '../../components/Button';
import { useAuth } from '../../hooks/auth';
import {
Container,
Logo,
Form,
FormTitle,
} from './styles';
const SignIn: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { signIn } = useAuth();
return (
Minha Carteira
);
}
export default SignIn;
================================================
FILE: src/pages/SignIn/styles.ts
================================================
import styled from 'styled-components';
export const Container = styled.div`
height: 100vh;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: ${props => props.theme.colors.primary};
`;
export const Logo = styled.div`
display: flex;
align-items: center;
margin-bottom: 30px;
> h2 {
color: ${props => props.theme.colors.white};
margin-left: 7px;
}
> img {
width: 40px;
height: 40px;
}
`;
export const Form = styled.form`
width: 300px;
height: 300px;
padding: 30px;
border-radius: 10px;
background-color: ${props => props.theme.colors.tertiary};
`;
export const FormTitle = styled.h1`
margin-bottom: 40px;
color: ${props => props.theme.colors.white};
&:after {
content: '';
display: block;
width: 55px;
border-bottom: 10px solid ${props => props.theme.colors.warning};
}
`;
================================================
FILE: src/react-app-env.d.ts
================================================
///
================================================
FILE: src/repositories/expenses.ts
================================================
export default [
{"description":"Energia elétrica", "amount":"150.55","type":"saída","frequency":"recorrente","date":"2020-01-10"},
{"description":"Água", "amount":"75.55", "type":"saída","frequency":"recorrente","date":"2020-01-15"},
{"description": "Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-01-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-01-23"},
{"description":"Compras do mês", "amount":"625.78","type":"saída","frequency":"recorrente","date":"2020-01-23"},
{"description":"Lanche", "amount":"45.70", "type":"saída","frequency":"eventual", "date":"2020-01-23"},
{"description":"Blusa", "amount":"95.70", "type":"saída","frequency":"eventual", "date":"2020-01-23"},
{"description":"Energia elétrica", "amount":"125.55","type":"saída","frequency":"recorrente","date":"2020-02-10"},
{"description":"Água", "amount":"90.15", "type":"saída","frequency":"recorrente","date":"2020-02-15"},
{"description":"Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-02-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-02-23"},
{"description":"Compras do mês", "amount":"540.00","type":"saída","frequency":"recorrente","date":"2020-02-25"},
{"description":"Parcela do Celular 1/5","amount":"150.99","type":"saída","frequency":"eventual", "date":"2020-02-26"},
{"description":"Cinema", "amount":"45.00", "type":"saída","frequency":"eventual", "date":"2020-02-23"},
{"description":"Energia elétrica", "amount":"97.00", "type":"saída","frequency":"recorrente","date":"2020-03-10"},
{"description":"Água", "amount":"100.10","type":"saída","frequency":"recorrente","date":"2020-03-15"},
{"description":"Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-03-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-03-23"},
{"description":"Compras do mês", "amount":"800.50","type":"saída","frequency":"recorrente","date":"2020-03-17"},
{"description":"Parcela do Celular 2/5","amount":"150.99","type":"saída","frequency":"eventual", "date":"2020-03-18"},
{"description":"Troca de Oléo do carro","amount":"90.00", "type":"saída","frequency":"eventual", "date":"2020-03-23"},
{"description":"Energia elétrica", "amount":"75.99", "type":"saída","frequency":"recorrente","date":"2020-04-10"},
{"description":"Água", "amount":"80.33", "type":"saída","frequency":"recorrente","date":"2020-04-15"},
{"description":"Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-04-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-04-23"},
{"description":"Compras do mês", "amount":"600.00","type":"saída","frequency":"recorrente","date":"2020-04-25"},
{"description":"Parcela do Celular 3/5","amount":"150.99","type":"saída","frequency":"eventual", "date":"2020-04-26"},
{"description":"Ebook de React Js", "amount":"85.95", "type":"saída","frequency":"eventual", "date":"2020-04-13"},
{"description":"Energia elétrica", "amount":"125.55","type":"saída","frequency":"recorrente","date":"2020-05-10"},
{"description":"Água", "amount":"90.15", "type":"saída","frequency":"recorrente","date":"2020-05-15"},
{"description":"Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-05-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-05-23"},
{"description":"Compras do mês", "amount":"540.00","type":"saída","frequency":"recorrente","date":"2020-05-25"},
{"description":"Parcela do Celular 4/5","amount":"150.99","type":"saída","frequency":"eventual", "date":"2020-05-26"},
{"description":"Blusa Iron Man", "amount":"150.00","type":"saída","frequency":"eventual", "date":"2020-01-23"},
{"description":"Energia elétrica", "amount":"200.00","type":"saída","frequency":"recorrente","date":"2020-06-10"},
{"description":"Água", "amount":"150.00","type":"saída","frequency":"recorrente","date":"2020-06-15"},
{"description":"Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-06-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-06-23"},
{"description":"Compras do mês", "amount":"559.15","type":"saída","frequency":"recorrente","date":"2020-06-25"},
{"description":"Parcela do Celular 5/5","amount":"150.99","type":"saída","frequency":"eventual", "date":"2020-06-26"},
{"description":"Perfume", "amount":"250.00","type":"saída","frequency":"eventual", "date":"2020-06-21"},
{"description":"Energia elétrica", "amount":"250.00","type":"saída","frequency":"recorrente","date":"2020-07-10"},
{"description":"Água", "amount":"90.00", "type":"saída","frequency":"recorrente","date":"2020-07-15"},
{"description":"Telefone", "amount":"99.99", "type":"saída","frequency":"recorrente","date":"2020-07-23"},
{"description":"Plano de Saúde", "amount":"300.00","type":"saída","frequency":"recorrente","date":"2020-07-23"},
{"description":"Compras do mês", "amount":"700.00","type":"saída","frequency":"recorrente","date":"2020-07-25"},
{"description":"Cafeteira", "amount":"250.00","type":"saída","frequency":"eventual", "date":"2020-07-26"},
{"description":"Pizza", "amount":"60.00", "type":"saída","frequency":"eventual", "date":"2020-07-19"},
{"description":"Pizza", "amount":"750.01", "type":"saída","frequency":"eventual", "date":"2020-07-19"}
]
================================================
FILE: src/repositories/gains.ts
================================================
export default [
{ "description": "Salário", "amount": "1300.52", "type": "entrada", "frequency": "recorrente", "date": "2020-01-10" },
{ "description": "Freela", "amount": "150.13", "type": "entrada", "frequency": "eventual", "date": "2020-01-17" },
{ "description": "Salário", "amount": "2500.23", "type": "entrada", "frequency": "recorrente", "date": "2020-02-10" },
{ "description": "Freela site", "amount": "900.23", "type": "entrada", "frequency": "eventual", "date": "2020-02-21" },
{ "description": "Freela app", "amount": "950.92", "type": "entrada", "frequency": "eventual", "date": "2020-02-23" },
{ "description": "Salário", "amount": "2500.25", "type": "entrada", "frequency": "recorrente", "date": "2020-03-10" },
{ "description": "Salário", "amount": "2500.18", "type": "entrada", "frequency": "recorrente", "date": "2020-04-10" },
{ "description": "Salário", "amount": "2500.15", "type": "entrada", "frequency": "recorrente", "date": "2020-05-10" },
{ "description": "Salário", "amount": "2500.12", "type": "entrada", "frequency": "recorrente", "date": "2020-06-10" },
{ "description": "Salário", "amount": "2500.00", "type": "entrada", "frequency": "recorrente", "date": "2020-07-10" },
]
================================================
FILE: src/routes/app.routes.tsx
================================================
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import Layout from '../components/Layout';
import Dashboard from '../pages/Dashboard';
import List from '../pages/List';
const AppRoutes: React.FC = () => (
);
export default AppRoutes;
================================================
FILE: src/routes/auth.routes.tsx
================================================
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import SignIn from '../pages/SignIn';
const AuthRoutes: React.FC = () => (
);
export default AuthRoutes;
================================================
FILE: src/routes/index.tsx
================================================
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { useAuth } from '../hooks/auth';
import App from './app.routes';
import Auth from './auth.routes';
const Routes: React.FC = () => {
const { logged } = useAuth();
return (
{ logged ? : }
);
}
export default Routes;
================================================
FILE: src/styles/GlobalStyles.ts
================================================
import { createGlobalStyle } from 'styled-components';
export default createGlobalStyle`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
height: 100%;
}
*, button, input {
border: 0;
outline: 0;
font-family: 'Roboto', sans-serif;
}
button {
cursor: pointer;
}
`;
================================================
FILE: src/styles/styled.d.ts
================================================
import 'styled-components';
declare module 'styled-components' {
export interface DefaultTheme {
title: string;
colors: {
primary: string;
secondary: string;
tertiary: string;
white: string;
black: string;
gray: string;
success: string;
info: string;
warning: string;
},
};
}
================================================
FILE: src/styles/themes/dark.ts
================================================
export default {
title: 'dark',
colors: {
primary: '#1B1F38',
secondary: '#252A48',
tertiary: '#313862',
white: '#FFF',
black: '#000',
gray: '#BFBFBF',
success: '#4E41F0',
info: '#F7931B',
warning: '#E44C4E',
},
};
================================================
FILE: src/styles/themes/light.ts
================================================
export default {
title: 'light',
colors: {
primary: '#DCDCDC',
secondary: '#FFFFFF',
tertiary: '#F5F5F5',
white: '#000',
black: '#FFF',
gray: '#BFBFBF',
success: '#03BB85',
info: '#3B5998',
warning: '#FF6961',
},
};
================================================
FILE: src/utils/emojis.ts
================================================
export default ['🤑', '🤩', '😍', '😎', '🤑', '😃', '😄'];
================================================
FILE: src/utils/formatCurrency.ts
================================================
const formatCurrency = (current: number): string => {
return current.toLocaleString(
'pt-br',
{
style: 'currency',
currency: 'BRL'
});
};
export default formatCurrency;
================================================
FILE: src/utils/formatDate.ts
================================================
const formatDate = (date: string): string => {
const dateFormatted = new Date(date);
const year = dateFormatted.getFullYear();
const day = dateFormatted.getDate() > 9
? dateFormatted.getDate() : `0${dateFormatted.getDate()}`;
const month = dateFormatted.getMonth() + 1 > 9
? dateFormatted.getMonth() + 1 : `0${dateFormatted.getMonth() + 1}`;
return `${day}/${month}/${year}`;
};
export default formatDate;
================================================
FILE: src/utils/months.ts
================================================
export default [
'Janeiro',
'Fevereiro',
'Março',
'Abril',
'Maio',
'Junho',
'Julho',
'Agosto',
'Setembro',
'Outubro',
'Novembro',
'Dezembro'
];
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
}