diff --git a/.eslintrc.json b/.eslintrc.json index dc8fa20..ebf25b4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -30,6 +30,7 @@ "alphabetize": { "order": "asc", "ignoreCase": true } } ], - "@next/next/no-img-element": "off" + "@next/next/no-img-element": "off", + "@typescript-eslint/no-explicit-any": "off" } } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..473feae --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,25 @@ +name: deploy + +on: + push: + branches: + - master + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Dependencies + run: | + yarn + + - name: Build + run: yarn build + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@4.1.1 + with: + branch: release # The branch the action should deploy to. + folder: out # The folder the action should deploy diff --git a/src/app/(public)/adicionar-cartao/page.tsx b/src/app/(public)/adicionar-cartao/page.tsx new file mode 100644 index 0000000..ddfb651 --- /dev/null +++ b/src/app/(public)/adicionar-cartao/page.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { + bankAccounts as bankAccountsData, + cardProviders as cardProvidersData, +} from "assets/data"; +import { Header } from "components/Header"; +import { makeSetData } from "components/Inputs/helpers"; +import { MoneyInput } from "components/Inputs/Money"; +import { SelectInput } from "components/Inputs/Select"; +import { TextInput } from "components/Inputs/Text"; +import { Space } from "components/Space"; +import { useState } from "react"; +import { CardTypeEnum } from "types/enums/card-type"; +import { PayAtEnum } from "types/enums/pay-at"; + +const bankAccounts = Object.values(bankAccountsData); + +interface NewCardForm { + cardType?: CardTypeEnum; + cardProviderId?: string; + lastFourDigits: string; + + // Credit cards + dueDay?: string; + limit?: number; + payAt?: PayAtEnum; + payWithBankAccountId?: string; + + // Debit cards + bankAccountId?: string; + + // VA, VR, VT + balance?: number; +} + +const AddCard = () => { + const [state, setState] = useState({ + lastFourDigits: "", + }); + + const setData = makeSetData({ + setState, + }); + + const cardProviders = Object.values(cardProvidersData).filter( + (p) => p.type === state.cardType, + ); + + return ( + <> +
+ +
+
+ setData("cardType", val)} + /> + + {state.cardType && ( + <> + setData("cardProviderId", val)} + /> + + + + setData("lastFourDigits", val)} + /> + + + + )} + + {state.cardType === CardTypeEnum.CREDIT && state.cardProviderId && ( + <> + ({ id: d }))} + fieldNames={{ + id: "id", + label: "id", + }} + value={state.dueDay} + onChange={(val) => setData("dueDay", val)} + /> + + + + setData("limit", val)} + /> + + + + setData("payAt", val)} + /> + + + + setData("payWithBankAccountId", val)} + /> + + )} + + {state.cardType === CardTypeEnum.DEBIT && state.cardProviderId && ( + <> + setData("bankAccountId", val)} + /> + + )} + + {[CardTypeEnum.VA, CardTypeEnum.VR, CardTypeEnum.VT].includes( + state.cardType as any, + ) && + state.cardProviderId && ( + <> + setData("balance", val)} + /> + + )} + + + + + +
+ + ); +}; + +export default AddCard; diff --git a/src/app/(public)/adicionar-conta/page.tsx b/src/app/(public)/adicionar-conta/page.tsx new file mode 100644 index 0000000..b9b78d6 --- /dev/null +++ b/src/app/(public)/adicionar-conta/page.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { bankProviders as bankProvidersData } from "assets/data"; +import { Header } from "components/Header"; +import { makeSetData } from "components/Inputs/helpers"; +import { MoneyInput } from "components/Inputs/Money"; +import { SelectInput } from "components/Inputs/Select"; +import { TextInput } from "components/Inputs/Text"; +import { Space } from "components/Space"; +import { useState } from "react"; + +const bankProviders = Object.values(bankProvidersData); + +interface NewAccountForm { + bankId?: string; + accountNumber: string; + branch: string; + balance: number; + name: string; +} + +const AddAccount = () => { + const [state, setState] = useState({ + accountNumber: "", + branch: "", + balance: 0, + name: "", + }); + + const setData = makeSetData({ + setState, + }); + + return ( + <> +
+ +
+
+ setData("bankId", val)} + /> + + + + setData("accountNumber", val)} + /> + + + + setData("branch", val)} + /> + + + + setData("balance", val)} + /> + + + + setData("name", val)} + /> + + + + + +
+ + ); +}; + +export default AddAccount; diff --git a/src/app/(public)/carteira/page.tsx b/src/app/(public)/carteira/page.tsx index 98dc422..c981fc5 100644 --- a/src/app/(public)/carteira/page.tsx +++ b/src/app/(public)/carteira/page.tsx @@ -7,6 +7,7 @@ import { import { Header } from "components/Header"; import { Space } from "components/Space"; import { WalletItem } from "components/WalletItem"; +import Link from "next/link"; import { PiPlusCircleBold } from "react-icons/pi"; import { CardTypeEnum } from "types/enums/card-type"; import { formatBankAccount } from "utils/format"; @@ -16,6 +17,10 @@ const bankAccounts = Object.values(bankAccountsData); const cards = Object.values(cardsData); const Wallet = () => { + const hasVA = cards.find((c) => c.type === CardTypeEnum.VA); + const hasVR = cards.find((c) => c.type === CardTypeEnum.VR); + const hasVT = cards.find((c) => c.type === CardTypeEnum.VT); + return ( <>
@@ -34,61 +39,69 @@ const Wallet = () => { )} -
-
- Bancos - - {formatMoney( - bankAccounts.reduce((acc, cur) => acc + cur.balance, 0), - )} - -
- -
- Vale Alimentação - - {formatMoney( - cards.reduce((acc, cur) => { - if (cur.type === CardTypeEnum.VA) { - return acc + (cur.balance || 0); - } - - return acc; - }, 0), - )} - -
+ {(hasVA || hasVA || hasVT) && ( +
+
+ Bancos + + {formatMoney( + bankAccounts.reduce((acc, cur) => acc + cur.balance, 0), + )} + +
+ + {hasVA && ( +
+ Vale Alimentação + + {formatMoney( + cards.reduce((acc, cur) => { + if (cur.type === CardTypeEnum.VA) { + return acc + (cur.balance || 0); + } + + return acc; + }, 0), + )} + +
+ )} -
- Vale Refeição - - {formatMoney( - cards.reduce((acc, cur) => { - if (cur.type === CardTypeEnum.VR) { - return acc + (cur.balance || 0); - } - - return acc; - }, 0), - )} - -
+ {hasVR && ( +
+ Vale Refeição + + {formatMoney( + cards.reduce((acc, cur) => { + if (cur.type === CardTypeEnum.VR) { + return acc + (cur.balance || 0); + } + + return acc; + }, 0), + )} + +
+ )} -
- Vale Transporte - - {formatMoney( - cards.reduce((acc, cur) => { - if (cur.type === CardTypeEnum.VT) { - return acc + (cur.balance || 0); - } - - return acc; - }, 0), - )} - + {hasVT && ( +
+ Vale Transporte + + {formatMoney( + cards.reduce((acc, cur) => { + if (cur.type === CardTypeEnum.VT) { + return acc + (cur.balance || 0); + } + + return acc; + }, 0), + )} + +
+ )}
-
+ )}
Fatura total @@ -124,9 +137,9 @@ const Wallet = () => { /> ))} - +
@@ -138,7 +151,7 @@ const Wallet = () => {
{cards.map((c) => ( { /> ))} - +
diff --git a/src/app/(public)/login/e-mail/page.tsx b/src/app/(public)/login/e-mail/page.tsx index 3e82b65..7cf93cd 100644 --- a/src/app/(public)/login/e-mail/page.tsx +++ b/src/app/(public)/login/e-mail/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Input } from "components/Input"; +import { EmailInput } from "components/Inputs/Email"; import { useRouter } from "next/navigation"; import { useForm, useFormState } from "react-hook-form"; @@ -39,14 +39,11 @@ const Email = () => { className="flex items-center justify-center flex-col gap-6" onSubmit={handleSubmit(onSubmit)} > - {}} /> + + + ); +}; + +export default Phone; diff --git a/src/app/(public)/login/telefone/success/page.tsx b/src/app/(public)/login/telefone/success/page.tsx new file mode 100644 index 0000000..18b1131 --- /dev/null +++ b/src/app/(public)/login/telefone/success/page.tsx @@ -0,0 +1,44 @@ +"use client"; + +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; + +const Success = () => { + const searchParams = useSearchParams(); + const email = searchParams.get("e-mail"); + + return ( + <> +
+

+ E-mail enviado com sucesso. +

+
+
+
+ Enviado para: + {email} +

Feche essa pagina e continue do seu e-mail.

+
+
+ + + Trocar e-mail + +
+
+ + ); +}; + +export default Success; diff --git a/src/assets/colors.ts b/src/assets/colors.ts index 08da267..85e2f8c 100644 --- a/src/assets/colors.ts +++ b/src/assets/colors.ts @@ -1,4 +1,6 @@ export const colors = { + primary: "#00c9fe", + red: "#ff0000", yellow: "#ffda29", green: "#008000", diff --git a/src/assets/data.ts b/src/assets/data.ts index fdf33c9..8593fde 100644 --- a/src/assets/data.ts +++ b/src/assets/data.ts @@ -1,6 +1,6 @@ -import { BankAccount } from "types/bank"; +import { Bank, BankAccount } from "types/bank"; import { Budget } from "types/budget"; -import { Card } from "types/card"; +import { Card, CardProvider } from "types/card"; import { Category } from "types/category"; import { CardTypeEnum } from "types/enums/card-type"; import { NetworkEnum } from "types/enums/network"; @@ -171,14 +171,32 @@ export const budget: Budget = { }, }; -export const bankAccounts: Record = { +export const bankProviders: Record = { + bradesco: { + bankId: "bradesco", + name: "Bradesco", + code: "123", + iconUrl: + "https://logospng.org/download/bradesco/logo-bradesco-escudo-1024.png", + color: "#F0152D", + }, nubank: { - bankAccountId: "nubank", - accountId: "foo1", bankId: "nubank", + name: "Nubank", + code: "321", iconUrl: "https://seucreditodigital.com.br/wp-content/uploads/2021/05/nova-logomarca-do-Nubank-721x720.jpg", color: "#820BD1", + }, +}; + +export const bankAccounts: Record = { + nubank: { + bankAccountId: "nubank", + accountId: "foo1", + bankId: bankProviders.nubank.bankId, + iconUrl: bankProviders.nubank.iconUrl, + color: bankProviders.nubank.color, includeOnBalance: true, isSystemManaged: false, accountNumber: "337420", @@ -189,10 +207,9 @@ export const bankAccounts: Record = { bradesco: { bankAccountId: "bradesco", accountId: "foo1", - bankId: "bradesco", - iconUrl: - "https://logospng.org/download/bradesco/logo-bradesco-escudo-1024.png", - color: "#F0152D", + bankId: bankProviders.bradesco.bankId, + iconUrl: bankProviders.bradesco.iconUrl, + color: bankProviders.bradesco.color, includeOnBalance: true, isSystemManaged: false, accountNumber: "123456", @@ -202,6 +219,29 @@ export const bankAccounts: Record = { }, }; +export const cardProviders: Record = { + xpInfinityOne: { + cardProviderId: "xpInfinityOne", + name: "Xp Infinity One", + iconUrl: + "https://logodownload.org/wp-content/uploads/2019/07/xp-investimentos-logo-8.png", + color: "#1B1A1D", + type: CardTypeEnum.CREDIT, + network: NetworkEnum.VISA, + statementDays: 14, + availableDueDates: ["05", "10", "15"], + }, + aleloRefeicao: { + cardProviderId: "aleloRefeicao", + name: "Alelo Refeicao", + iconUrl: + "https://logodownload.org/wp-content/uploads/2017/09/alelo-logo-1-599x393.png", + color: "#017958", + type: CardTypeEnum.VA, + network: NetworkEnum.ELO, + }, +}; + export const cards: Record = { xp: { cardId: "xp", diff --git a/src/components/AddTransaction/index.tsx b/src/components/AddTransaction/index.tsx index e3514e2..cc10a6f 100644 --- a/src/components/AddTransaction/index.tsx +++ b/src/components/AddTransaction/index.tsx @@ -6,8 +6,9 @@ import { colors } from "assets/colors"; import { bankAccounts, cards, categories as categoriesData } from "assets/data"; import { DateInput } from "components/Inputs/Date"; import { MoneyInput } from "components/Inputs/Money"; -import { Select } from "components/Inputs/Select"; +import { SelectInput } from "components/Inputs/Select"; import { TextInput } from "components/Inputs/Text"; +import { TextareaInput } from "components/Inputs/Textarea"; import { Space } from "components/Space"; import { useAddTransaction } from "contexts/add-transaction"; import { TransactionTypeEnum } from "types/enums/transaction-type"; @@ -44,10 +45,10 @@ export const AddTransaction = () => { onClick={() => close()} />
- { - { - onChange(e.target.value)} + /> +
+ ); +} diff --git a/src/components/Inputs/Money/index.tsx b/src/components/Inputs/Money/index.tsx index 7e5fd34..23bd75a 100644 --- a/src/components/Inputs/Money/index.tsx +++ b/src/components/Inputs/Money/index.tsx @@ -1,43 +1,20 @@ -import React, { useState } from "react"; +import React from "react"; import { formatMoney } from "utils/format"; +import { makeBeforeChangeValue } from "../helpers"; + interface Props { label: string; - value: number; + value?: number; onChange: (val: number) => void; } export function MoneyInput({ label, value, onChange }: Props) { - const [state, setState] = useState(value); - - const beforeChangeValue = (keyPressed: string) => { - if (keyPressed === "Backspace") { - setState((prevState) => { - const valString = prevState.toString(); - - const val = parseInt( - valString.substring(0, valString.length - 1) || "0", - 10, - ); - - onChange(val); - - return val; - }); - - return; - } - - if (!/^\d$/.test(keyPressed)) return; - - setState((prevState) => { - const val = parseInt(`${prevState}${keyPressed}`, 10); - - onChange(val); - - return val; - }); - }; + const beforeChangeValue = makeBeforeChangeValue({ + numeric: true, + setValueAsNumber: true, + onChange, + }); return (
@@ -50,8 +27,8 @@ export function MoneyInput({ label, value, onChange }: Props) { autoComplete="off" placeholder={label} className="input input-bordered mt-2" - value={formatMoney(state)} - onKeyUp={(v) => beforeChangeValue(v.key)} + value={formatMoney(value)} + onChange={(e) => beforeChangeValue(e.target.value)} />
); diff --git a/src/components/Inputs/Phone/index.tsx b/src/components/Inputs/Phone/index.tsx new file mode 100644 index 0000000..b245b39 --- /dev/null +++ b/src/components/Inputs/Phone/index.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { CountryEnum } from "types/enums/country"; +import { formatPhone } from "utils/format"; + +import { makeBeforeChangeValue } from "../helpers"; + +interface Props { + id?: string; + country: CountryEnum; + label: string; + placeholder?: string; + value: string; + onChange: (val: string) => void; +} + +const maxlengthByCountry = { + [CountryEnum.BR]: 11, +}; + +export function PhoneInput({ + id, + label, + placeholder, + value, + country, + + onChange, +}: Props) { + const beforeChangeValue = makeBeforeChangeValue({ + numeric: true, + maxLength: maxlengthByCountry[country], + onChange, + }); + + return ( +
+ + beforeChangeValue(e.target.value)} + /> +
+ ); +} diff --git a/src/components/Inputs/Select/index.tsx b/src/components/Inputs/Select/index.tsx index ca44967..09a6d0e 100644 --- a/src/components/Inputs/Select/index.tsx +++ b/src/components/Inputs/Select/index.tsx @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Listbox, Transition } from "@headlessui/react"; +import { colors } from "assets/colors"; import { Icon } from "components/Icon"; import React from "react"; import { Fragment } from "react"; @@ -12,26 +13,26 @@ interface Props { label: string; toBeSelectedLabel?: string; data: Array; - selectedId?: string; + value?: string; fieldNames: { id: keyof T; label: keyof T; - color: keyof T; + color?: keyof T; icon?: keyof T; iconUrl?: keyof T; }; onChange: (val: any) => void; } -export function Select>({ +export function SelectInput>({ label, toBeSelectedLabel, data, - selectedId, + value, fieldNames, onChange, }: Props) { - const selected = data.find((d) => d[fieldNames.id] === selectedId); + const selected = data.find((d) => d[fieldNames.id] === value); return ( @@ -63,8 +64,14 @@ export function Select>({ style={ selected ? { - backgroundColor: selected[fieldNames.color], - color: getTextColor(selected[fieldNames.color]), + backgroundColor: fieldNames.color + ? selected[fieldNames.color] + : colors.primary, + color: getTextColor( + fieldNames.color + ? selected[fieldNames.color] + : colors.primary, + ), } : {} } @@ -137,14 +144,26 @@ export function Select>({
diff --git a/src/components/Inputs/Text/index.tsx b/src/components/Inputs/Text/index.tsx index 2afc47e..8119f7c 100644 --- a/src/components/Inputs/Text/index.tsx +++ b/src/components/Inputs/Text/index.tsx @@ -1,42 +1,68 @@ import React from "react"; +import { formatBankAccount } from "utils/format"; + +import { makeBeforeChangeValue } from "../helpers"; interface Props { + id?: string; label: string; + numeric?: boolean; placeholder?: string; - textarea?: boolean; + mask?: "99999-9"; + maxLength?: number; value: string; onChange: (val: string) => void; } export function TextInput({ + id, label, placeholder, - textarea, value, + mask, + + numeric, + maxLength, + onChange, }: Props) { + const beforeChangeValue = makeBeforeChangeValue({ + numeric, + maxLength, + onChange, + }); + + const formatValue = (val: string): string => { + if (!mask) return val; + + switch (mask) { + case "99999-9": + return formatBankAccount(val); + + default: + return val; + } + }; + return (
- {textarea ? ( -