diff --git a/package-lock.json b/package-lock.json index 571f529..38bd184 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2176,6 +2176,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/src/App.tsx b/src/App.tsx index 45a3ec7..0314c5b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,12 @@ import NotFound from "./pages/NotFound" import { AuthProvider } from "./hooks/useAuth" import { ChakraProvider } from "@chakra-ui/react" import { system } from "./theme" +import Profile from "./pages/Profile" +import ProfileEdit from "./pages/ProfileEdit" +import Warnings from "./pages/Warnings" +import Loans from "./pages/Loans" +import { Toaster } from "./components/ui/toaster" +import PrivateRoute from './PrivateRoute'; function App() { return ( @@ -13,11 +19,16 @@ function App() { } /> - } /> } /> + } /> + } /> + } /> + } /> + } /> + ) } diff --git a/src/PrivateRoute.tsx b/src/PrivateRoute.tsx new file mode 100644 index 0000000..02c55af --- /dev/null +++ b/src/PrivateRoute.tsx @@ -0,0 +1,17 @@ +import { useNavigate } from "react-router"; +import { useAuth } from "./hooks/useAuth" +import { useEffect } from "react"; + +const PrivateRoute = ({ children }: { children: any }) => { + const navigate = useNavigate(); + const { token } = useAuth(); + + useEffect(() => { + if (token) return; + navigate('/cadastro'); + }) + + return children; +} + +export default PrivateRoute \ No newline at end of file diff --git a/src/components/NavBar/index.tsx b/src/components/NavBar/index.tsx new file mode 100644 index 0000000..694c1e1 --- /dev/null +++ b/src/components/NavBar/index.tsx @@ -0,0 +1,39 @@ +import { Box, Center, Flex, Stack, Text } from "@chakra-ui/react" +import { Button } from "../ui/button" +import { BsHouse, BsBell, BsBook, BsPerson } from 'react-icons/bs'; +import { useLocation, useNavigate } from "react-router"; + +export const NavBar = () => { + const navigate = useNavigate() + const location = useLocation() + + const selectedTab = location.pathname.replace('/', ''); + const tabs = [ + { label: 'Início', value: 'inicio', icon: }, + { label: 'Empréstimos', value: 'emprestimos', icon: }, + { label: 'Avisos', value: 'avisos', icon: }, + { label: 'Perfil', value: 'perfil', icon: }, + ] + + return ( + +
+ + {tabs.map(tab => ( + + ))} + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..df6c2c3 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,43 @@ +"use client" + +import { + Toaster as ChakraToaster, + Portal, + Spinner, + Stack, + Toast, + createToaster, +} from "@chakra-ui/react" + +export const toaster = createToaster({ + placement: "bottom-end", + pauseOnPageIdle: true, +}) + +export const Toaster = () => { + return ( + + + {(toast) => ( + + {toast.type === "loading" ? ( + + ) : ( + + )} + + {toast.title && {toast.title}} + {toast.description && ( + {toast.description} + )} + + {toast.action && ( + {toast.action.label} + )} + {toast.meta?.closable && } + + )} + + + ) +} diff --git a/src/hooks/useApi/index.tsx b/src/hooks/useApi/index.tsx index 02c2ad0..16e343f 100644 --- a/src/hooks/useApi/index.tsx +++ b/src/hooks/useApi/index.tsx @@ -15,10 +15,8 @@ const createApiInstance = (url: string) => { const getDefaultErrorUseAPIMessage = (err: any) => { return { error: true, - ...err?.toJSON?.call(), ...err?.response, ...err?.response?.data, - ...err?.data, }; }; @@ -30,6 +28,18 @@ const useApi = () => { ); return { + getProfile: (token: string): Promise<{ data: User }> => { + return new Promise((resolve) => { + api + .get('/auth/profile', { + headers: { + Authorization: `Bearer ${token}`, + } + }) + .then((res) => resolve(res)) + .catch((err) => resolve(getDefaultErrorUseAPIMessage(err))); + }); + }, signUp: (data: { firstName: string; lastName: string; @@ -46,6 +56,33 @@ const useApi = () => { .then((res) => resolve(res)) .catch((err) => resolve(getDefaultErrorUseAPIMessage(err))); }); + }, + editProfile: async (id: string, data: { + firstName: string; + lastName: string; + email: string; + phone: string; + oldPassword?: string + newPassword?: string + }): Promise<{ data: { + id: string; + } }> => { + return new Promise((resolve) => { + api + .put(`/users/${id}`, data) + .then((res) => resolve(res)) + .catch((err) => resolve(getDefaultErrorUseAPIMessage(err))); + }); + }, + deleteProfile: async (id: string): Promise<{ data: { + id: string; + } }> => { + return new Promise((resolve) => { + api + .delete(`/users/${id}`) + .then((res) => resolve(res)) + .catch((err) => resolve(getDefaultErrorUseAPIMessage(err))); + }); } } } diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 320e7a8..afec0e3 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -3,6 +3,7 @@ import { createContext, useState, useContext, ReactNode } from 'react'; import useApi from './useApi'; +import { toaster } from '../components/ui/toaster'; interface SignUpParams { firstName: string; @@ -12,17 +13,27 @@ interface SignUpParams { password: string; } +interface EditProfileParams { + firstName: string; + lastName: string; + email: string; + phone: string; + oldPassword?: string; + newPassword?: string; +} + type AuthContextType = { isAuthenticated: boolean; token: string | null; signOut: () => void; - signUp: (userToSignUp: SignUpParams) => Promise; + signUp: (userToSignUp: SignUpParams) => Promise; + editProfile: (id: string, profileToEdit: EditProfileParams) => Promise; }; const AuthContext = createContext({} as AuthContextType); export const AuthProvider = ({ children }: { children: ReactNode }) => { - const { signUp: authSignUp } = useApi(); + const { signUp: authSignUp, editProfile: authEditProfile } = useApi(); const localToken = typeof window !== 'undefined' @@ -32,13 +43,38 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { localToken ? localToken : null, ); - async function signUp(userToSignUp: SignUpParams) { + async function signUp(userToSignUp: SignUpParams): Promise { const { data } = await authSignUp(userToSignUp); - if (!data.accessToken) return; + if (!data.accessToken) { + toaster.create({ + title: 'Erro ao criar conta', + description: 'Verifique os campos e tente novamente.', + type: 'error', + }) + return false; + }; setToken(data.accessToken); if (typeof window !== 'undefined') { localStorage.setItem('@livrolivre:token', data.accessToken); } + return true; + } + + async function editProfile(id: string, profileToEdit: EditProfileParams): Promise { + const { data } = await authEditProfile(id, profileToEdit); + if (data.id) { + toaster.create({ + title: 'Perfil editado com sucesso!', + type: 'success', + }) + return true; + }; + toaster.create({ + title: 'Erro ao editar perfil', + description: 'Verifique os campos e tente novamente.', + type: 'error', + }) + return false; } function signOut(): void { @@ -55,6 +91,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { token, signOut, signUp, + editProfile, }} > {children} diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts index 4e95785..5494ac9 100644 --- a/src/interfaces/user.ts +++ b/src/interfaces/user.ts @@ -2,6 +2,7 @@ export interface User { id: string; firstName: string; lastName: string; + phone: string; email: string; createdAt: string; updatedAt: string; diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index e56e6c1..df41cf7 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,10 +1,11 @@ -import styles from './styles'; +import { Box } from '@chakra-ui/react'; +import { NavBar } from '../../components/NavBar'; function Home() { return ( - -

Home

-
+ + + ); } diff --git a/src/pages/Home/styles.ts b/src/pages/Home/styles.ts deleted file mode 100644 index 3667ffd..0000000 --- a/src/pages/Home/styles.ts +++ /dev/null @@ -1,5 +0,0 @@ -import styled from 'styled-components'; - -const Container = styled.div``; - -export default { Container }; diff --git a/src/pages/Loans/index.tsx b/src/pages/Loans/index.tsx new file mode 100644 index 0000000..4adb083 --- /dev/null +++ b/src/pages/Loans/index.tsx @@ -0,0 +1,12 @@ +import { Box } from '@chakra-ui/react'; +import { NavBar } from '../../components/NavBar'; + +function Loans() { + return ( + + + + ); +} + +export default Loans diff --git a/src/pages/Profile/DeleteProfileDialog/index.tsx b/src/pages/Profile/DeleteProfileDialog/index.tsx new file mode 100644 index 0000000..7011fc9 --- /dev/null +++ b/src/pages/Profile/DeleteProfileDialog/index.tsx @@ -0,0 +1,68 @@ +import { HStack, Text } from "@chakra-ui/react" +import { + DialogContent, + DialogRoot, + DialogTitle, + DialogTrigger, + DialogHeader, + DialogFooter, + DialogBody, + DialogActionTrigger, + DialogCloseTrigger, +} from "../../../components/ui/dialog" +import { Button } from "../../../components/ui/button" +import useApi from "../../../hooks/useApi" +import { useAuth } from "../../../hooks/useAuth" +import { useNavigate } from "react-router" + +const DeleteProfileDialog = () => { + const navigate = useNavigate(); + const { deleteProfile, getProfile } = useApi(); + const { token, signOut } = useAuth(); + + const handleDelete = async () => { + console.log('maia', 123) + if (!token) return; + const { data } = await getProfile(token); + await deleteProfile(data.id); + signOut(); + navigate('/login'); + } + + return ( + + + + + + + + Excluir conta + + + + Tem certeza que deseja excluir sua conta? Essa ação é irreversível. + + + + + + + + + + + + + ) +} + +export default DeleteProfileDialog \ No newline at end of file diff --git a/src/pages/Profile/index.tsx b/src/pages/Profile/index.tsx new file mode 100644 index 0000000..e09ef94 --- /dev/null +++ b/src/pages/Profile/index.tsx @@ -0,0 +1,54 @@ +import { Box, Center, Text, Stack } from '@chakra-ui/react'; +import { NavBar } from '../../components/NavBar'; +import { Button } from '../../components/ui/button'; +import { useNavigate } from 'react-router'; +import DeleteProfileDialog from './DeleteProfileDialog'; +import { useAuth } from '../../hooks/useAuth'; + +function Profile() { + + const navigate = useNavigate(); + const { signOut } = useAuth(); + + return ( + + +
+ + Perfil + + + + + + +
+
+ + +
+ ); +} + +export default Profile diff --git a/src/pages/ProfileEdit/ProfileEditForm/index.tsx b/src/pages/ProfileEdit/ProfileEditForm/index.tsx new file mode 100644 index 0000000..4109dd9 --- /dev/null +++ b/src/pages/ProfileEdit/ProfileEditForm/index.tsx @@ -0,0 +1,143 @@ +import { useEffect, useState } from 'react'; +import { useAuth } from '../../../hooks/useAuth'; +import { Input, Stack } from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; +import { PasswordInput } from '../../../components/ui/password-input'; +import { Button } from '../../../components/ui/button'; +import useApi from '../../../hooks/useApi'; +import { Field } from '../../../components/ui/field'; + +interface FormValues { + firstName: string; + lastName: string; + email: string; + phone: string; + oldPassword?: string; + newPassword?: string; + newPasswordConfirmation?: string; +} + +function SignUpForm() { + const [userId, setUserId] = useState(''); + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + setValue, + watch, + formState: { errors, isValid }, + } = useForm(); + + const { editProfile, token } = useAuth(); + const { getProfile } = useApi(); + + const getUserData = async () => { + if (!token) return; + const { data } = await getProfile(token); + setValue('firstName', data.firstName); + setValue('lastName', data.lastName); + setValue('email', data.email); + setValue('phone', data.phone); + setUserId(data.id) + } + + useEffect(() => { + getUserData(); + }, []) + + const onSubmit = handleSubmit(async (data: FormValues) => { + setLoading(true); + await editProfile(userId, { + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + phone: data.phone, + newPassword: data.newPassword, + oldPassword: data.oldPassword, + }); + setLoading(false); + }) + + return ( +
+ + + + + + + + + + + + + + + + + + value === watch('newPassword') || 'As senhas não coincidem.', + })} + /> + + + + +
+ ); +} + +export default SignUpForm diff --git a/src/pages/ProfileEdit/ProfileEditHeader/index.tsx b/src/pages/ProfileEdit/ProfileEditHeader/index.tsx new file mode 100644 index 0000000..490d3e4 --- /dev/null +++ b/src/pages/ProfileEdit/ProfileEditHeader/index.tsx @@ -0,0 +1,18 @@ +import { Flex, Stack, Text } from '@chakra-ui/react'; +import { BsArrowLeftShort } from 'react-icons/bs'; +import { useNavigate } from 'react-router'; + +function SignUpHeader() { + const navigate = useNavigate(); + return ( + + navigate('/perfil')}> + + Voltar + + Editar perfil + + ); +} + +export default SignUpHeader diff --git a/src/pages/ProfileEdit/index.tsx b/src/pages/ProfileEdit/index.tsx new file mode 100644 index 0000000..4bbb2ca --- /dev/null +++ b/src/pages/ProfileEdit/index.tsx @@ -0,0 +1,18 @@ +import { Box, Center, Stack } from '@chakra-ui/react'; +import ProfileEditForm from './ProfileEditForm'; +import ProfileEditHeader from './ProfileEditHeader'; + +function ProfileEdit() { + return ( + +
+ + + + +
+
+ ); +} + +export default ProfileEdit diff --git a/src/pages/SignUp/SignUpForm/index.tsx b/src/pages/SignUp/SignUpForm/index.tsx index 4b3fab8..08805ad 100644 --- a/src/pages/SignUp/SignUpForm/index.tsx +++ b/src/pages/SignUp/SignUpForm/index.tsx @@ -5,6 +5,7 @@ import { Input, Stack } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { PasswordInput } from '../../../components/ui/password-input'; import { Button } from '../../../components/ui/button'; +import { Field } from '../../../components/ui/field'; interface FormValues { firstName: string; @@ -22,6 +23,7 @@ function SignUpForm() { const { register, handleSubmit, + formState: { errors, isValid }, } = useForm(); const { signUp, token } = useAuth(); @@ -40,43 +42,67 @@ function SignUpForm() { useEffect(() => { if (!token) return; - navigate('/'); + navigate('/inicio'); }, [token]) return (
- - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/pages/Warnings/index.tsx b/src/pages/Warnings/index.tsx new file mode 100644 index 0000000..2e0c5dd --- /dev/null +++ b/src/pages/Warnings/index.tsx @@ -0,0 +1,12 @@ +import { Box } from '@chakra-ui/react'; +import { NavBar } from '../../components/NavBar'; + +function Warnings() { + return ( + + + + ); +} + +export default Warnings diff --git a/src/theme.ts b/src/theme.ts index b77c86d..3526bb5 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -10,6 +10,9 @@ const customConfig = defineConfig({ green: { 100: { value: '#037030' }, }, + red: { + 100: { value: '#F2542D' }, + }, }, }, },