diff --git a/.DS_Store b/.DS_Store index 9f24cd0..7b548ba 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 74b7586..cb034b6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ yarn-error.log* .env.production.local # vercel -.vercel +.vercel \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3662b37 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/components/Box/index.tsx b/components/Box/index.tsx new file mode 100644 index 0000000..622e382 --- /dev/null +++ b/components/Box/index.tsx @@ -0,0 +1,31 @@ +import { Box as ChakraBox, BoxProps as ChakraBoxProps, Text, Flex } from '@chakra-ui/react'; +import { ReactNode } from 'react'; + +type BoxProps = { + title?: string; + description?: string; + children: ReactNode; +} & ChakraBoxProps; + +export const Box = ({ title, description, children, ...rest }: BoxProps) => { + return ( + + {(title || description) && ( + + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + + )} + + {children} + + ); +}; diff --git a/components/DatePicker/index.tsx b/components/DatePicker/date-picker.component.tsx similarity index 74% rename from components/DatePicker/index.tsx rename to components/DatePicker/date-picker.component.tsx index aa8e024..a823e99 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/date-picker.component.tsx @@ -1,6 +1,8 @@ import React, { HTMLAttributes } from 'react'; -import { FormControl, FormLabel, FormErrorMessage, useColorMode } from '@chakra-ui/react'; import ReactDatePicker from 'react-datepicker'; +import { FormControl, FormLabel, FormErrorMessage, useColorMode } from '@chakra-ui/react'; + +import { useIsMobile } from '@Modules/BaseModule/hooks/useIsMobile'; interface Props { isClearable?: boolean; @@ -11,6 +13,8 @@ interface Props { label?: string; error?: string; value?: string; + maxDate?: Date; + minDate?: Date; } export const DatePicker = ({ @@ -22,20 +26,27 @@ export const DatePicker = ({ label, error, value, + maxDate, + minDate, ...rest }: Props & HTMLAttributes) => { const isLight = useColorMode().colorMode === 'light'; + const isMobile = useIsMobile(); + return ( {label && {label}} -
+
{error && {error}} diff --git a/components/If/if.component.tsx b/components/If/if.component.tsx new file mode 100644 index 0000000..1e461c8 --- /dev/null +++ b/components/If/if.component.tsx @@ -0,0 +1,6 @@ +import { ReactElement, ReactNode } from 'react'; + +export const If = ({ condition, children }: { condition: boolean; children: ReactElement }) => { + if (condition) return children; + return null; +}; diff --git a/components/LateralMenu/index.tsx b/components/LateralMenu/index.tsx new file mode 100644 index 0000000..1bbc53e --- /dev/null +++ b/components/LateralMenu/index.tsx @@ -0,0 +1,166 @@ +import React, { ReactNode, ReactText } from 'react'; +import { + IconButton, + Box, + CloseButton, + Flex, + Icon, + useColorModeValue, + Button, + Link, + Drawer, + DrawerContent, + Text, + useDisclosure, + BoxProps, + FlexProps, + Badge, +} from '@chakra-ui/react'; +import { FiHome, FiPower, FiTrendingUp, FiTrendingDown, FiMenu } from 'react-icons/fi'; +import { IconType } from 'react-icons'; +import Image from 'next/image'; +import { BiSolidCategoryAlt } from 'react-icons/bi'; + +interface LinkItemProps { + name: string; + icon: IconType; + link: string; +} +const LinkItems: Array = [ + { name: 'Início', icon: FiHome, link: '/' }, + { name: 'Criar receita', icon: FiTrendingUp, link: '/bill?type=INCOME' }, + { name: 'Criar despesa', icon: FiTrendingDown, link: '/bill?type=EXPENSE' }, + { name: 'Categorias', icon: BiSolidCategoryAlt, link: '/category' }, + { + name: 'Sair', + icon: FiPower, + link: '/logout', + }, +]; + +export const LateralMenu = ({ children }: { children: ReactNode }) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + return ( + + onClose} display={{ base: 'none', md: 'block' }} /> + + + + + + {/* mobilenav */} + + + {children} + + + ); +}; + +interface SidebarProps extends BoxProps { + onClose: () => void; +} + +const SidebarContent = ({ onClose, ...rest }: SidebarProps) => { + return ( + + + + + + Beta + + + + + {LinkItems.map(({ name, icon, link }) => ( + + {name} + + ))} + + ); +}; + +interface NavItemProps extends FlexProps { + icon: IconType; + children: ReactText; + link: string; +} +const NavItem = ({ icon, link, children, ...rest }: NavItemProps) => { + return ( + + + {icon && ( + + )} + {children} + + + ); +}; + +interface MobileProps extends FlexProps { + onOpen: () => void; +} +const MobileNav = ({ onOpen, ...rest }: MobileProps) => { + return ( + + } /> + + + + + + Beta + + + + + ); +}; diff --git a/components/Modal/index.tsx b/components/Modal/index.tsx new file mode 100644 index 0000000..eb07131 --- /dev/null +++ b/components/Modal/index.tsx @@ -0,0 +1,31 @@ +import { + Modal as ModalChakra, + ModalOverlay, + ModalContent, + ModalBody, + ModalCloseButton, + ModalHeader, + ModalFooter, +} from '@chakra-ui/react'; + +interface ModalInterface { + onClose: () => void; + isOpen: boolean; + title?: string; + footer?: React.ReactElement; + children: React.ReactElement; +} + +export const Modal = ({ title, footer, onClose, isOpen, children }: ModalInterface) => { + return ( + + + + + {title && {title}} + {children} + {footer && {footer}} + + + ); +}; diff --git a/components/MoneyInput/index.tsx b/components/MoneyInput/index.tsx index cf40d71..106183b 100644 --- a/components/MoneyInput/index.tsx +++ b/components/MoneyInput/index.tsx @@ -2,13 +2,11 @@ import { FormControl, FormLabel, FormErrorMessage, - NumberInputProps, NumberInput, NumberInputField, - NumberInputStepper, - NumberIncrementStepper, - NumberDecrementStepper, FormControlProps, + InputLeftAddon, + InputGroup, } from '@chakra-ui/react'; import { forwardRef, LegacyRef } from 'react'; @@ -19,18 +17,17 @@ type MoneyInputProps = FormControlProps & { }; export const MoneyInput = forwardRef( - ({ name, label, error, ...rest }: MoneyInputProps, ref) => { + ({ name, label, error, isRequired, ...rest }: MoneyInputProps, ref) => { return ( {label && {label}} - }> - - - - - - + + R$ + + } borderLeftRadius={0} /> + + {error && {error}} ); diff --git a/components/RadioCard/radio-card.component.tsx b/components/RadioCard/radio-card.component.tsx new file mode 100644 index 0000000..7c9f6c4 --- /dev/null +++ b/components/RadioCard/radio-card.component.tsx @@ -0,0 +1,34 @@ +import { Box, useRadio } from '@chakra-ui/react'; + +export const RadioCard = props => { + const { getInputProps, getCheckboxProps } = useRadio(props); + + const input = getInputProps(); + const checkbox = getCheckboxProps(); + + return ( + + + + {props.children} + + + ); +}; diff --git a/components/index.ts b/components/index.ts index 0653a6d..16b44ae 100644 --- a/components/index.ts +++ b/components/index.ts @@ -1,4 +1,9 @@ export * from './MoneyInput'; export * from './Input'; export * from './Select'; -export * from './DatePicker'; +export * from './DatePicker/date-picker.component'; +export * from './Modal'; +export * from './LateralMenu'; +export * from './Box'; +export * from './RadioCard/radio-card.component'; +export * from './If/if.component'; diff --git a/configs/Firebase.ts b/configs/Firebase.ts index 9829e38..8c5d3a9 100644 --- a/configs/Firebase.ts +++ b/configs/Firebase.ts @@ -16,6 +16,9 @@ if (!firebase.apps.length) { } else { firebase.app(); } + export default firebase; export const firestore = firebase.firestore(); export const auth = firebase.auth(); + +export const googleProvider = new firebase.auth.GoogleAuthProvider(); diff --git a/configs/theme.tsx b/configs/theme.tsx index 4a365e4..3a4bc4a 100644 --- a/configs/theme.tsx +++ b/configs/theme.tsx @@ -5,6 +5,8 @@ const config: ThemeConfig = { useSystemColorMode: false, }; -const theme = extendTheme({ config }); +const theme = extendTheme({ + config, +}); export default theme; diff --git a/hoc/withLateralMenu.tsx b/hoc/withLateralMenu.tsx new file mode 100644 index 0000000..7bdc016 --- /dev/null +++ b/hoc/withLateralMenu.tsx @@ -0,0 +1,11 @@ +import { LateralMenu } from '@Components/LateralMenu'; + +const withLateralMenu = Children => { + return props => ( + + + + ); +}; + +export default withLateralMenu; diff --git a/modules/Authentication/container/FormForgotPassword/index.tsx b/modules/Authentication/container/FormForgotPassword/index.tsx deleted file mode 100644 index 28dcdeb..0000000 --- a/modules/Authentication/container/FormForgotPassword/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useForm } from 'react-hook-form'; -import { Button } from '@chakra-ui/button'; -import { Flex, Box, useToast } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; - -import AuthenticationService from '@Authentication/services/AuthenticationService'; -import { Input } from '@Components'; - -const FormForgotPassword = () => { - const toast = useToast(); - - const { - handleSubmit, - register, - formState: { errors, isSubmitting }, - } = useForm(); - const Router = useRouter(); - - const onSubmit = async values => { - try { - const haveSentMail = await AuthenticationService.forgotPassword(values?.email); - if (haveSentMail) { - toast({ - description: 'An e-mail with password recovery link has been sent!', - status: 'success', - }); - - Router.replace('/'); - } - } catch (err) { - toast({ - description: err.message, - status: 'error', - }); - } - }; - - return ( - -
- - - -
- - -
-
-
-
- ); -}; - -export default FormForgotPassword; diff --git a/modules/Authentication/container/FormLogin/form-login.container.tsx b/modules/Authentication/container/FormLogin/form-login.container.tsx new file mode 100644 index 0000000..daee7e1 --- /dev/null +++ b/modules/Authentication/container/FormLogin/form-login.container.tsx @@ -0,0 +1,53 @@ +import { Button } from '@chakra-ui/button'; +import { Flex, Box, useToast, Text, Spacer, Image, AbsoluteCenter } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; +import { FcGoogle } from 'react-icons/fc'; + +import { signInWithGoogle } from '@Authentication/services/authentication.service'; +import { useUser } from '@Modules/Authentication/context/UserContext'; + +export const FormLoginContainer = () => { + const toast = useToast(); + const router = useRouter(); + const { userId, setUserId } = useUser(); + + useEffect(() => { + if (userId) { + router.push('/'); + } + }, [router, userId]); + + const onLoginWithGoogle = async () => { + try { + const uid = await signInWithGoogle(); + + if (uid) { + setUserId(uid); + router.push('/'); + } + } catch (err) { + toast({ + description: err.message, + status: 'error', + }); + } + }; + + return ( + + + + + Desbrave o caminho para a estabilidade financeira e saia do vermelho. + + + + + ); +}; diff --git a/modules/Authentication/container/FormLogin/index.tsx b/modules/Authentication/container/FormLogin/index.tsx deleted file mode 100644 index ce5ed91..0000000 --- a/modules/Authentication/container/FormLogin/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useForm } from 'react-hook-form'; -import { Button } from '@chakra-ui/button'; -import { Flex, Box, useToast } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; -import Link from 'next/link'; -import { useEffect } from 'react'; - -import AuthenticationService from '@Authentication/services/AuthenticationService'; -import User from '@Authentication/interfaces/User.interface'; -import { useUser } from '@Modules/Authentication/context/UserContext'; -import { Input } from '@Components'; - -const FormLogin = () => { - const toast = useToast(); - const { - handleSubmit, - register, - formState: { errors, isSubmitting }, - } = useForm(); - const Router = useRouter(); - const { userId, setUserId } = useUser(); - - useEffect(() => { - if (userId) { - Router.replace('/'); - } - }, [userId]); - - const onSubmit = async values => { - try { - const { uid } = (await AuthenticationService.signIn(values?.email, values?.password)) as User; - if (uid) { - setUserId(uid); - Router.replace('/'); - } - } catch (err) { - toast({ - description: err.message, - status: 'error', - }); - } - }; - - return ( - -
- - - - - -
- - - - - - - - -
-
-
-
- ); -}; - -export default FormLogin; diff --git a/modules/Authentication/container/FormSignUp/index.tsx b/modules/Authentication/container/FormSignUp/index.tsx deleted file mode 100644 index 8e47736..0000000 --- a/modules/Authentication/container/FormSignUp/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useForm } from 'react-hook-form'; -import { Button } from '@chakra-ui/button'; -import { useToast, Flex, Box } from '@chakra-ui/react'; -import { useRouter } from 'next/router'; - -import { Input } from '@Components'; -import { auth } from '@Configs/Firebase'; -import { useUser } from '@Modules/Authentication/context/UserContext'; - -const FormSignUp = () => { - const { - handleSubmit, - register, - formState: { errors, isSubmitting }, - } = useForm(); - const Router = useRouter(); - const toast = useToast(); - const { setUserId } = useUser(); - - const onSubmit = async values => { - try { - const { user } = await auth.createUserWithEmailAndPassword(values.email, values.password); - - localStorage.setItem('USER_UID', user.uid); - setUserId(user.uid); - Router.replace('/'); - - toast({ - description: 'User successful created!', - status: 'success', - }); - } catch (error) { - console.error(error); - } - }; - - return ( - -
- - - - - -
- -
-
-
-
- ); -}; - -export default FormSignUp; diff --git a/modules/Authentication/context/UserContext.tsx b/modules/Authentication/context/UserContext.tsx index 77f2142..df6a3a6 100644 --- a/modules/Authentication/context/UserContext.tsx +++ b/modules/Authentication/context/UserContext.tsx @@ -15,11 +15,6 @@ interface UserProviderProps { children: React.ReactNode; } -const formatAuthUser = user => ({ - uid: user.uid, - email: user.email, -}); - export const UserProvider = ({ children }: UserProviderProps) => { const [userId, setUserId] = useState(null); const [user, setUser] = useState(null); @@ -31,9 +26,7 @@ export const UserProvider = ({ children }: UserProviderProps) => { return; } - // var formattedUser = formatAuthUser(authState); setUserId(authState.uid); - // setUser(formatAuthUser); }; useEffect(() => { diff --git a/modules/Authentication/services/AuthenticationService.ts b/modules/Authentication/services/AuthenticationService.ts deleted file mode 100644 index 9de8d30..0000000 --- a/modules/Authentication/services/AuthenticationService.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { auth } from '@Configs/Firebase'; - -import User from '../interfaces/User.interface'; - -export default class AuthenticationService { - constructor() {} - - static signIn(email: string, password: string): Promise { - return auth.signInWithEmailAndPassword(email, password).then( - ({ user }) => - ({ - uid: user.uid, - } as User) - ); - } - - static signUp(email: string, password: string): Promise { - return auth.createUserWithEmailAndPassword(email, password).then( - ({ user }) => - ({ - uid: user.uid, - } as User) - ); - } - - static forgotPassword(email: string): Promise { - return auth.sendPasswordResetEmail(email).then(_ => true); - } -} diff --git a/modules/Authentication/services/authentication.service.ts b/modules/Authentication/services/authentication.service.ts new file mode 100644 index 0000000..5f96907 --- /dev/null +++ b/modules/Authentication/services/authentication.service.ts @@ -0,0 +1,33 @@ +import { auth, firestore, googleProvider } from '@Configs/Firebase'; +import { createDefaultCategories } from '@Modules/Category/helpers/createDefaultCategories'; + +interface IGoogleProviderProfile { + email?: string; + family_name?: string; + given_name?: string; + granted_scopes?: string; + id?: string; + locale?: string; + name?: string; + picture?: string; +} + +export const signInWithGoogle = () => { + return auth.signInWithPopup(googleProvider).then(async ({ additionalUserInfo, user }) => { + if (additionalUserInfo.isNewUser) { + const profile = additionalUserInfo.profile as IGoogleProviderProfile; + + await firestore + .collection('user') + .doc(user.uid) + .set({ + email: profile?.email, + name: `${profile?.given_name} ${profile?.family_name}`, + locale: profile?.locale, + image: profile?.picture, + }) + .then(() => createDefaultCategories(user.uid)); + } + return user.uid; + }); +}; diff --git a/modules/BaseModule/components/Bubble/index.tsx b/modules/BaseModule/components/Bubble/index.tsx new file mode 100644 index 0000000..d9e57b1 --- /dev/null +++ b/modules/BaseModule/components/Bubble/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { FieldErrors, FieldValues, UseFormRegister } from 'react-hook-form'; + +import { Input, Select } from '@Components'; +import SelectOption from '@Modules/BaseModule/interfaces/SelectOption'; + +import { BubbleEnum, BUBBLE_TYPES } from '../../constants/Bubble'; + +interface BubbleInterface { + type: BubbleEnum; + name: string; + label: string; + register: UseFormRegister; + errors: FieldErrors; + required?: boolean; + props?: object; + options?: SelectOption[]; + value?: (string | number | readonly string[]) & string; +} + +const Bubble = ({ + type, + name, + label, + register, + errors, + required = false, + options = [], + value, + props = {}, +}: BubbleInterface) => { + switch (type) { + case BUBBLE_TYPES.INPUT_TEXT: + return ( + + ); + + case BUBBLE_TYPES.INPUT_COLOR: + return ( + + ); + + case BUBBLE_TYPES.SELECT: + return ( + { setValue('year', +select.target.value)} + error={errors.year?.message} + mb={2} + /> + + + + + + ( + field.onChange(date)} + selected={field.value} + defaultValue={new Date().getTime()} + style={{ marginBottom: '10px' }} + error={errors.dueDate?.message} + /> + )} + /> + + + + + + + Recorrência + + + + + + Contas reccorentes serão criadas com a frequência que você determinar. + + + + + - - - - ( - field.onChange(date)} - selected={field.value} - style={{ marginBottom: '10px' }} - /> - )} - /> - - - -