├── src ├── lib │ ├── index.ts │ └── api.ts ├── contexts │ ├── index.ts │ └── app-context.ts ├── helpers │ ├── index.ts │ └── get-token-helper.ts ├── vite-env.d.ts ├── styles │ └── global.css ├── hooks │ ├── index.ts │ ├── use-app-context-hook.ts │ └── use-products-page-hook.ts ├── pages │ ├── index.ts │ ├── orders-page.tsx │ ├── home-page.tsx │ └── cart-page.tsx ├── interfaces │ ├── department-interface.ts │ ├── pagination-interface.ts │ ├── product-interface.ts │ ├── category-interface.ts │ ├── user-interface.ts │ ├── cart-interface.ts │ ├── index.ts │ └── order-interface.ts ├── services │ ├── index.ts │ ├── category-service.ts │ ├── department-service.ts │ ├── product-service.ts │ ├── order-service.ts │ ├── user-service.ts │ └── cart-service.ts ├── components │ ├── index.ts │ ├── private-route-component.tsx │ ├── pagination-component.tsx │ ├── navbar-component.tsx │ ├── app-component.tsx │ ├── sign-up-component.tsx │ └── sign-in-component.tsx └── main.tsx ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── postcss.config.js ├── tsconfig.json ├── vite.config.ts ├── tailwind.config.js ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── package.json └── README.md /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | -------------------------------------------------------------------------------- /src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app-context' 2 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-token-helper' 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Build 2 | build/ 3 | 4 | # Dependencies 5 | node_modules 6 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@rocketseat/eslint-config/node" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | dist/ 3 | 4 | # Dependencies 5 | node_modules/ 6 | 7 | # Env 8 | .env 9 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-app-context-hook' 2 | export * from './use-products-page-hook' 3 | -------------------------------------------------------------------------------- /src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cart-page' 2 | export * from './home-page' 3 | export * from './orders-page' 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const api = axios.create({ 4 | baseURL: 'http://localhost:3333', 5 | }) 6 | -------------------------------------------------------------------------------- /src/interfaces/department-interface.ts: -------------------------------------------------------------------------------- 1 | export interface Department { 2 | id: string 3 | name: string 4 | description: string 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/pagination-interface.ts: -------------------------------------------------------------------------------- 1 | export interface Pagination { 2 | count: number 3 | limit: number 4 | currentPage: number 5 | pagesCount: number 6 | } 7 | -------------------------------------------------------------------------------- /src/helpers/get-token-helper.ts: -------------------------------------------------------------------------------- 1 | import cookies from 'js-cookie' 2 | 3 | export function getToken(): string { 4 | return cookies.get('NaturaChallenge:Token') || '' 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/product-interface.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | id: string 3 | name: string 4 | description: string 5 | price: number 6 | stockQuantity: number 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/interfaces/category-interface.ts: -------------------------------------------------------------------------------- 1 | import { Department } from '../interfaces' 2 | 3 | export interface Category { 4 | id: string 5 | name: string 6 | description: string 7 | department: Department 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/user-interface.ts: -------------------------------------------------------------------------------- 1 | import { Cart } from '../interfaces' 2 | 3 | export interface User { 4 | id: string 5 | email: string 6 | name: string 7 | role: string 8 | cart: Cart 9 | } 10 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cart-service' 2 | export * from './category-service' 3 | export * from './department-service' 4 | export * from './order-service' 5 | export * from './product-service' 6 | export * from './user-service' 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './index.html', 5 | './src/**/*.tsx', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app-component' 2 | export * from './navbar-component' 3 | export * from './pagination-component' 4 | export * from './private-route-component' 5 | export * from './sign-in-component' 6 | export * from './sign-up-component' 7 | -------------------------------------------------------------------------------- /src/interfaces/cart-interface.ts: -------------------------------------------------------------------------------- 1 | export interface CartItem { 2 | id: string 3 | product: { 4 | id: string 5 | name: string 6 | price: number 7 | } 8 | quantity: number 9 | } 10 | 11 | export interface Cart { 12 | id: string 13 | items: CartItem[] 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cart-interface' 2 | export * from './category-interface' 3 | export * from './department-interface' 4 | export * from './order-interface' 5 | export * from './pagination-interface' 6 | export * from './product-interface' 7 | export * from './user-interface' 8 | -------------------------------------------------------------------------------- /src/interfaces/order-interface.ts: -------------------------------------------------------------------------------- 1 | export interface OrderItem { 2 | id: string 3 | product: { 4 | id: string 5 | name: string 6 | price: number 7 | } 8 | price: number 9 | quantity: number 10 | } 11 | 12 | export interface Order { 13 | id: string 14 | items: OrderItem[] 15 | price: number 16 | } 17 | -------------------------------------------------------------------------------- /src/services/category-service.ts: -------------------------------------------------------------------------------- 1 | import { Category } from '../interfaces' 2 | import { api } from '../lib' 3 | 4 | export async function fetchCategories(): Promise { 5 | const { data } = await api.get('/categories', { 6 | params: { 7 | skip: 0, 8 | take: 23, 9 | }, 10 | }) 11 | 12 | return data.categories 13 | } 14 | -------------------------------------------------------------------------------- /src/services/department-service.ts: -------------------------------------------------------------------------------- 1 | import { Department } from '../interfaces' 2 | import { api } from '../lib' 3 | 4 | export async function fetchDepartments(): Promise { 5 | const { data } = await api.get('/departments', { 6 | params: { 7 | skip: 0, 8 | take: 6, 9 | }, 10 | }) 11 | 12 | return data.departments 13 | } 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | natura-challenge-ui 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/hooks/use-app-context-hook.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { AppContext } from '../contexts/app-context' 4 | 5 | export function useAppContext() { 6 | const appContext = useContext(AppContext) 7 | 8 | if (!appContext) { 9 | throw new Error('useAppContext must be used within an AppProvider') 10 | } 11 | 12 | return appContext 13 | } 14 | -------------------------------------------------------------------------------- /src/components/private-route-component.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { Navigate } from 'react-router-dom' 3 | 4 | import { useAppContext } from '../hooks/use-app-context-hook' 5 | 6 | interface PrivateRouteProps { 7 | children: ReactNode 8 | } 9 | 10 | export function PrivateRoute({ children }: PrivateRouteProps) { 11 | const { user } = useAppContext() 12 | 13 | if (!user) { 14 | return 15 | } 16 | 17 | return children 18 | } 19 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | 5 | import { App } from './components' 6 | import './styles/global.css' 7 | 8 | const queryClient = new QueryClient() 9 | 10 | createRoot(document.getElementById('root')!).render( 11 | 12 | 13 | 14 | 15 | , 16 | ) 17 | -------------------------------------------------------------------------------- /src/contexts/app-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, SetStateAction } from 'react' 2 | 3 | import { User } from '../interfaces' 4 | 5 | interface AppContextProps { 6 | user: User | null 7 | setUser: Dispatch> 8 | signInIsVisible: boolean 9 | setSignInIsVisible: Dispatch> 10 | signUpIsVisible: boolean 11 | setSignUpIsVisible: Dispatch> 12 | } 13 | 14 | export const AppContext = createContext({} as AppContextProps) 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/orders-page.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { Order } from '../interfaces' 3 | import { getOrdersHistory } from '../services' 4 | 5 | export function Orders() { 6 | const { 7 | data: orders, 8 | isLoading, 9 | error, 10 | } = useQuery({ 11 | queryKey: ['products'], 12 | queryFn: getOrdersHistory, 13 | }) 14 | 15 | if (isLoading) { 16 | return Carregando... 17 | } 18 | 19 | if (error) { 20 | return Erro ao carregar os pedidos 21 | } 22 | 23 | return
{JSON.stringify(orders, null, 2)}
24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /src/services/product-service.ts: -------------------------------------------------------------------------------- 1 | import { Pagination, Product } from '../interfaces' 2 | import { api } from '../lib' 3 | 4 | interface Params { 5 | skip: number 6 | take: number 7 | departmentId?: string 8 | categoryId?: string 9 | } 10 | 11 | interface Response { 12 | products: Product[] 13 | pagination: Pagination 14 | } 15 | 16 | export async function fetchProducts(params: Params): Promise { 17 | const { skip, take, departmentId, categoryId } = params 18 | 19 | const { data } = await api.get('/products', { 20 | params: { 21 | skip, 22 | take, 23 | departmentId, 24 | categoryId, 25 | }, 26 | }) 27 | 28 | return data 29 | } 30 | -------------------------------------------------------------------------------- /src/services/order-service.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from '../helpers' 2 | import { Order } from '../interfaces' 3 | import { api } from '../lib' 4 | 5 | const token = getToken() 6 | 7 | export async function createOrder(cartId: string): Promise { 8 | const response = await api.post( 9 | '/orders', 10 | { 11 | cartId, 12 | }, 13 | { 14 | headers: { 15 | Authorization: `Bearer ${token}`, 16 | }, 17 | }, 18 | ) 19 | 20 | return response.data 21 | } 22 | 23 | export async function getOrdersHistory() { 24 | const response = await api.get('/users/orders-history', { 25 | headers: { 26 | Authorization: `Bearer ${token}`, 27 | }, 28 | }) 29 | 30 | return response.data 31 | } 32 | -------------------------------------------------------------------------------- /src/services/user-service.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../interfaces' 2 | import { api } from '../lib' 3 | 4 | interface Params { 5 | email: string 6 | password: string 7 | } 8 | 9 | interface Response { 10 | token: string 11 | } 12 | 13 | export async function signIn(params: Params): Promise { 14 | const { email, password } = params 15 | 16 | const response = await api.post('/sign-in', { 17 | email, 18 | password, 19 | }) 20 | 21 | return response.data 22 | } 23 | 24 | export async function getAuthenticatedUser(token: string): Promise { 25 | const response = await api.get('/me', { 26 | headers: { 27 | Authorization: `Bearer ${token}`, 28 | }, 29 | }) 30 | 31 | return response.data 32 | } 33 | -------------------------------------------------------------------------------- /src/components/pagination-component.tsx: -------------------------------------------------------------------------------- 1 | interface PaginationProps { 2 | currentPage: number 3 | handleNextPage: () => void 4 | handlePreviousPage: () => void 5 | pagesCount: number 6 | } 7 | 8 | export function Pagination(props: PaginationProps) { 9 | const { currentPage, handleNextPage, handlePreviousPage, pagesCount } = props 10 | 11 | return ( 12 |
13 | 20 | 21 | Página {currentPage} de {pagesCount} 22 | 23 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "natura-challenge-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc -b && vite build", 8 | "dev": "vite", 9 | "lint": "eslint src --ext .ts --fix", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^2.1.8", 14 | "@hookform/resolvers": "^3.9.0", 15 | "@tanstack/react-query": "^5.56.2", 16 | "axios": "^1.7.7", 17 | "js-cookie": "^3.0.5", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "react-hook-form": "^7.53.0", 21 | "react-router-dom": "^6.26.2", 22 | "zod": "^3.23.8" 23 | }, 24 | "devDependencies": { 25 | "@rocketseat/eslint-config": "^2.2.2", 26 | "@types/js-cookie": "^3.0.6", 27 | "@types/react": "^18.3.3", 28 | "@types/react-dom": "^18.3.0", 29 | "@vitejs/plugin-react": "^4.3.1", 30 | "autoprefixer": "^10.4.20", 31 | "eslint": "^8.57.0", 32 | "globals": "^15.9.0", 33 | "postcss": "^8.4.47", 34 | "tailwindcss": "^3.4.11", 35 | "typescript": "^5.5.3", 36 | "vite": "^5.4.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/services/cart-service.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from '../helpers' 2 | import { Cart } from '../interfaces' 3 | import { api } from '../lib' 4 | 5 | const token = getToken() 6 | 7 | export async function addItemToCart( 8 | cartId: string, 9 | productId: string, 10 | quantity: number, 11 | ): Promise { 12 | const response = await api.post( 13 | '/carts/add-item', 14 | { 15 | cartId, 16 | productId, 17 | quantity: Number(quantity), 18 | }, 19 | { 20 | headers: { 21 | Authorization: `Bearer ${token}`, 22 | }, 23 | }, 24 | ) 25 | 26 | return response.data 27 | } 28 | 29 | export async function clearCart(cartId: string): Promise { 30 | const response = await api.put( 31 | '/carts/clear', 32 | { 33 | cartId, 34 | }, 35 | { 36 | headers: { 37 | Authorization: `Bearer ${token}`, 38 | }, 39 | }, 40 | ) 41 | 42 | return response.data 43 | } 44 | 45 | export async function removeItemToCart( 46 | cartId: string, 47 | cartItemId: string, 48 | ): Promise { 49 | const response = await api.put( 50 | '/carts/remove-item', 51 | { 52 | cartId, 53 | cartItemId, 54 | }, 55 | { 56 | headers: { 57 | Authorization: `Bearer ${token}`, 58 | }, 59 | }, 60 | ) 61 | 62 | return response.data 63 | } 64 | 65 | export async function updateItemQuantity( 66 | cartId: string, 67 | cartItemId: string, 68 | quantity: number, 69 | ): Promise { 70 | const response = await api.put( 71 | '/carts/update-item-quantity', 72 | { 73 | cartId, 74 | cartItemId, 75 | quantity, 76 | }, 77 | { 78 | headers: { 79 | Authorization: `Bearer ${token}`, 80 | }, 81 | }, 82 | ) 83 | 84 | return response.data 85 | } 86 | -------------------------------------------------------------------------------- /src/components/navbar-component.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import { useAppContext } from '../hooks' 3 | 4 | export function Navbar() { 5 | const { setSignInIsVisible, user } = useAppContext() 6 | 7 | function handleOpenSignIn() { 8 | setSignInIsVisible(true) 9 | } 10 | 11 | return ( 12 |
13 |
14 |
15 | 16 | natura-challenge-ui 17 | 18 | 19 | {!user ? ( 20 | 26 | ) : ( 27 | 49 | )} 50 |
51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/components/app-component.tsx: -------------------------------------------------------------------------------- 1 | import cookies from 'js-cookie' 2 | import { useEffect, useState } from 'react' 3 | import { BrowserRouter, Route, Routes } from 'react-router-dom' 4 | 5 | import { Navbar, PrivateRoute, SignIn, SignUp } from '../components' 6 | import { AppContext } from '../contexts' 7 | import { User } from '../interfaces' 8 | import { Cart, Home, Orders } from '../pages' 9 | import { getAuthenticatedUser } from '../services' 10 | 11 | export function App() { 12 | const [user, setUser] = useState(null) 13 | const [signInIsVisible, setSignInIsVisible] = useState(false) 14 | const [signUpIsVisible, setSignUpIsVisible] = useState(false) 15 | 16 | useEffect(() => { 17 | const token = cookies.get('NaturaChallenge:Token') 18 | 19 | if (token) { 20 | getAuthenticatedUser(token).then((user) => setUser(user)) 21 | } 22 | }, []) 23 | 24 | return ( 25 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | } /> 42 | 43 | 47 | 48 | 49 | } 50 | /> 51 | 52 | 56 | 57 | 58 | } 59 | /> 60 | 61 |
62 |
63 | 64 | 65 | 66 |
67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /src/hooks/use-products-page-hook.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | import { useState } from 'react' 3 | 4 | import { useAppContext } from '../hooks' 5 | import { Category, Department, Pagination, Product } from '../interfaces' 6 | import { 7 | addItemToCart, 8 | fetchCategories, 9 | fetchDepartments, 10 | fetchProducts, 11 | } from '../services' 12 | 13 | interface ProductsFilter { 14 | categoryId: string | undefined 15 | departmentId: string | undefined 16 | } 17 | 18 | export function useProductsPage() { 19 | const [currentPage, setCurrentPage] = useState(1) 20 | const [productsFilter, setProductsFilter] = useState({} as ProductsFilter) 21 | 22 | const itemsPerPage = 15 23 | const skip = (currentPage - 1) * itemsPerPage 24 | 25 | const { user, setUser } = useAppContext() 26 | 27 | const { 28 | data: departments, 29 | isLoading: isLoadingDepartments, 30 | error: errorDepartments, 31 | } = useQuery({ 32 | queryKey: ['departments'], 33 | queryFn: fetchDepartments, 34 | }) 35 | 36 | const { 37 | data: categories, 38 | isLoading: isLoadingCategories, 39 | error: errorCategories, 40 | } = useQuery({ 41 | queryKey: ['categories'], 42 | queryFn: fetchCategories, 43 | }) 44 | 45 | const { 46 | data: productsResponse, 47 | isLoading: isLoadingProducts, 48 | error: errorProducts, 49 | } = useQuery<{ products: Product[]; pagination: Pagination }>({ 50 | queryKey: [ 51 | 'products', 52 | { 53 | skip, 54 | take: itemsPerPage, 55 | departmentId: productsFilter.departmentId, 56 | categoryId: productsFilter.categoryId, 57 | }, 58 | ], 59 | queryFn: () => 60 | fetchProducts({ 61 | skip, 62 | take: itemsPerPage, 63 | departmentId: productsFilter.departmentId, 64 | categoryId: productsFilter.categoryId, 65 | }), 66 | }) 67 | 68 | const isLoading = 69 | isLoadingProducts || isLoadingCategories || isLoadingDepartments 70 | 71 | const error = 72 | errorProducts?.message || 73 | errorCategories?.message || 74 | errorDepartments?.message || 75 | null 76 | 77 | const products = productsResponse?.products || [] 78 | const pagesCount = productsResponse?.pagination.pagesCount || 1 79 | 80 | const handleNextPage = () => { 81 | if (productsResponse?.pagination && currentPage < pagesCount) { 82 | setCurrentPage((page) => page + 1) 83 | } 84 | } 85 | 86 | const handlePreviousPage = () => { 87 | setCurrentPage((page) => Math.max(page - 1, 1)) 88 | } 89 | 90 | const handleSelectDepartment = (department: Department) => { 91 | setProductsFilter({ 92 | departmentId: department.id, 93 | categoryId: undefined, 94 | }) 95 | 96 | setCurrentPage(1) 97 | } 98 | 99 | const handleSelectCategory = (category: Category) => { 100 | setProductsFilter({ 101 | categoryId: category.id, 102 | departmentId: undefined, 103 | }) 104 | 105 | setCurrentPage(1) 106 | } 107 | 108 | const handleAddToCart = async (product: Product) => { 109 | if (!user) { 110 | return 111 | } 112 | 113 | const cart = await addItemToCart(user.cart.id, product.id, 1) 114 | 115 | setUser({ 116 | ...user, 117 | cart, 118 | }) 119 | } 120 | 121 | return { 122 | categories, 123 | currentPage, 124 | departments, 125 | error, 126 | handleAddToCart, 127 | handleNextPage, 128 | handlePreviousPage, 129 | handleSelectCategory, 130 | handleSelectDepartment, 131 | isLoading, 132 | pagesCount, 133 | products, 134 | productsFilter, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/pages/home-page.tsx: -------------------------------------------------------------------------------- 1 | import { Pagination } from '../components' 2 | import { useAppContext, useProductsPage } from '../hooks' 3 | import { Product } from '../interfaces' 4 | 5 | export function Home() { 6 | const { setSignInIsVisible, user } = useAppContext() 7 | 8 | const { 9 | categories, 10 | currentPage, 11 | departments, 12 | error, 13 | handleAddToCart, 14 | handleNextPage, 15 | handlePreviousPage, 16 | isLoading, 17 | pagesCount, 18 | handleSelectCategory, 19 | handleSelectDepartment, 20 | products, 21 | productsFilter, 22 | } = useProductsPage() 23 | 24 | function onAddToCart(product: Product) { 25 | if (!user) { 26 | setSignInIsVisible(true) 27 | } else { 28 | handleAddToCart(product) 29 | } 30 | } 31 | 32 | if (isLoading) { 33 | return Carregando... 34 | } 35 | 36 | if (error) { 37 | return Erro ao carregar os produtos 38 | } 39 | 40 | return ( 41 |
42 | {/* Filter */} 43 | 70 | 71 |
72 |

Produtos

73 | 74 | {/* List */} 75 |
    76 | {products.length === 0 77 | ? 'Nenhum produto encontrado' 78 | : products.map((product) => ( 79 |
  • 83 |
    84 |

    {product.name}

    85 |

    {product.description}

    86 |

    87 | ${product.price.toFixed(2)} 88 |

    89 |
    90 | 96 |
  • 97 | ))} 98 |
99 | 100 | {products.length > 0 && ( 101 | 107 | )} 108 |
109 |
110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /src/components/sign-up-component.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react' 2 | 3 | import { useAppContext } from '../hooks' 4 | 5 | export function SignUp() { 6 | const { signUpIsVisible, setSignInIsVisible, setSignUpIsVisible } = 7 | useAppContext() 8 | 9 | function handleCloseSignUp() { 10 | setSignUpIsVisible(false) 11 | } 12 | 13 | function handleOpenSignIn() { 14 | setSignUpIsVisible(false) 15 | setSignInIsVisible(true) 16 | } 17 | 18 | return ( 19 | 24 | 28 | 29 |
30 |
31 | 35 |
36 |

37 | Criar conta 38 |

39 |
40 | 41 |
42 |
43 |
44 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 56 |
57 | 58 |
59 |
60 | 61 |
62 | 65 |
66 | 67 |
68 |
69 | 70 |
71 | 77 |
78 |
79 | 80 |

81 | Já tem uma conta?{' '} 82 | 86 | Entrar 87 | 88 |

89 |
90 |
91 |
92 |
93 |
94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /src/pages/cart-page.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext } from '../hooks' 2 | import { createOrder, removeItemToCart, updateItemQuantity } from '../services' 3 | 4 | export function Cart() { 5 | const { user, setUser } = useAppContext() 6 | 7 | const removeItem = async (itemId: string) => { 8 | if (!user) { 9 | return 10 | } 11 | 12 | const cart = await removeItemToCart(user.cart.id, itemId) 13 | 14 | setUser({ 15 | ...user, 16 | cart, 17 | }) 18 | } 19 | 20 | const calculateTotal = () => { 21 | return ( 22 | user?.cart.items 23 | .reduce((total, item) => total + item.product.price * item.quantity, 0) 24 | .toFixed(2) || '0.00' 25 | ) 26 | } 27 | 28 | async function handleQuantityChange(id: string, arg1: number) { 29 | if (!user) { 30 | return 31 | } 32 | 33 | const cart = await updateItemQuantity(user.cart.id, id, arg1) 34 | 35 | setUser({ 36 | ...user, 37 | cart, 38 | }) 39 | } 40 | 41 | async function handleCheckout() { 42 | if (!user) { 43 | return 44 | } 45 | 46 | await createOrder(user.cart.id) 47 | } 48 | 49 | return ( 50 |
51 | {/* Lista de Itens do Carrinho */} 52 |
53 |

Seu carrinho

54 | {user?.cart.items.length ? ( 55 |
    56 | {user.cart.items.map((item) => ( 57 |
  • 61 |
    62 |
    63 |

    64 | {item.product.name} 65 |

    66 | 67 |

    68 | R${item.product.price.toFixed(2)} 69 |

    70 |
    71 |
    72 |
    73 | 86 | 92 |
    93 |
  • 94 | ))} 95 |
96 | ) : ( 97 |

Seu carrinho está vazio.

98 | )} 99 |
100 | 101 | {/* Resumo do Pedido */} 102 | 130 |
131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📦 Teste Técnico Natura&Co (Desenvolvedor Sênior) 2 | 3 | Este é o repositório do frontend do projeto Natura Challenge, que complementa a [API desenvolvida](https://github.com/mateus-in/natura-challenge-api) para fornecer uma aplicação web completa de e-commerce. Este frontend foi criado usando **React** com **Vite** e está integrado com a API para oferecer funcionalidades como listagem de produtos, gerenciamento de carrinho, autenticação de usuários, entre outros. 4 | 5 | ## 🚀 Tecnologias Utilizadas 6 | 7 | - **React** para construção da interface de usuário 8 | - **Vite** para desenvolvimento e build rápido 9 | - **React Router** para gerenciamento de rotas 10 | - **React Hook Form** e **Zod** para gerenciamento e validação de formulários 11 | - **Axios** para requisições HTTP 12 | - **React Query** para gerenciamento de estado remoto e otimização de chamadas à API 13 | - **js-cookie** para manipulação de cookies e gerenciamento do token de autenticação 14 | 15 | ## 📁 Estrutura de Pastas 16 | 17 | A estrutura de pastas do projeto segue boas práticas de organização para garantir modularidade, escalabilidade e fácil manutenção. 18 | 19 | ```plaintext 20 | src/ 21 | ├── components # Componentes reutilizáveis (Header, Footer, etc.) 22 | ├── contexts # Contextos globais, como autenticação e carrinho 23 | ├── helpers # Funções auxiliares e utilitárias 24 | ├── hooks # Hooks personalizados para lógica de negócios 25 | ├── interfaces # Definições de interfaces TypeScript 26 | ├── lib # Instâncias e configurações de bibliotecas (ex: axios) 27 | ├── pages # Páginas principais do aplicativo (Home, Cart, SignIn, SignUp, etc.) 28 | ├── services # Serviços para interações com a API (auth, user, products, etc.) 29 | ├── styles # Estilos globais e configurações de CSS/Tailwind 30 | └── main.tsx # Arquivo principal de entrada do aplicativo 31 | ``` 32 | 33 | ### 📄 Explicação dos Diretórios 34 | 35 | - **components/**: Contém componentes reutilizáveis e modulares utilizados em várias partes do aplicativo. 36 | - **contexts/**: Armazena contextos globais que gerenciam o estado compartilhado, como autenticação e gerenciamento do carrinho. 37 | - **helpers/**: Inclui funções utilitárias e auxiliares usadas em todo o projeto. 38 | - **hooks/**: Hooks personalizados para encapsular e reutilizar lógica de negócios complexa. 39 | - **interfaces/**: Definições de interfaces TypeScript que descrevem tipos e contratos do projeto. 40 | - **lib/**: Configurações e instâncias de bibliotecas externas (ex: configuração do Axios). 41 | - **pages/**: Páginas principais que representam as diferentes rotas do aplicativo. 42 | - **services/**: Serviços para chamadas à API e lógica de negócios relacionada a essas operações. 43 | - **styles/**: Contém estilos globais, como arquivos CSS ou configurações do TailwindCSS. 44 | 45 | ## 📚 Casos de Uso 46 | 47 | ### Produtos 48 | 49 | - **Listar produtos**: Exibe todos os produtos disponíveis na loja. 50 | - **Filtrar produtos por departamento**: Permite filtrar produtos com base no departamento selecionado. 51 | - **Filtrar produtos por categoria**: Permite filtrar produtos com base na categoria selecionada. 52 | - **Adicionar produto ao carrinho**: Permite adicionar produtos ao carrinho de compras. 53 | - **Remover produto do carrinho**: Remove um produto específico do carrinho. 54 | - **Atualizar a quantidade de um determinado item do carrinho**: Permite ajustar a quantidade de um item específico no carrinho. 55 | - **Limpar o carrinho**: (A fazer) Remove todos os itens do carrinho de compras. 56 | - **Concluir uma compra e gerar um pedido**: Finaliza o pedido e gera uma ordem de compra. 57 | - **Listar os pedidos do usuário**: Exibe o histórico de pedidos do usuário autenticado. 58 | 59 | ### Autenticação 60 | 61 | - **Fazer login**: Autentica o usuário na plataforma e armazena o token de acesso. 62 | - **Cadastrar o usuário**: (A fazer) Registra um novo usuário na plataforma. 63 | 64 | ## 🛠️ Como Executar o Projeto 65 | 66 | ### Pré-requisitos 67 | 68 | - Node.js v16.x ou superior 69 | - API (https://github.com/mateus-in/natura-challenge-api) 70 | 71 | ### Instalação 72 | 73 | 1. Clone o repositório: 74 | 75 | ```bash 76 | git clone https://github.com/mateus-in/natura-challenge-ui.git 77 | cd natura-challenge-ui 78 | ``` 79 | 80 | 2. Instale as dependências: 81 | 82 | ```bash 83 | npm i 84 | ``` 85 | 86 | 3. Acesse a aplicação em: 87 | 88 | ```bash 89 | http://localhost:5173 90 | ``` 91 | 92 | 93 | ## 🧑‍💻 Usuário de Testes 94 | 95 | Para facilitar o teste da aplicação, você pode utilizar um dos usuários de testes já criados. Apenas certifique-se de que o banco de dados está rodando e que as seeds foram executadas. 96 | 97 | ### Usuário USER 98 | 99 | user1@naturachallenge.com.br 100 | naturachallengepass 101 | -------------------------------------------------------------------------------- /src/components/sign-in-component.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react' 2 | import { zodResolver } from '@hookform/resolvers/zod' 3 | import cookies from 'js-cookie' 4 | import { useForm } from 'react-hook-form' 5 | import { z } from 'zod' 6 | 7 | import { useAppContext } from '../hooks' 8 | import { getAuthenticatedUser, signIn } from '../services' 9 | 10 | const signInSchema = z.object({ 11 | email: z.string().email('Email inválido'), 12 | password: z.string(), 13 | }) 14 | 15 | type SignInFormProps = z.infer 16 | 17 | export function SignIn() { 18 | const { signInIsVisible, setSignInIsVisible, setSignUpIsVisible, setUser } = 19 | useAppContext() 20 | 21 | const { 22 | register, 23 | handleSubmit, 24 | formState: { errors }, 25 | } = useForm({ 26 | resolver: zodResolver(signInSchema), 27 | }) 28 | 29 | function handleCloseSignIn() { 30 | setSignInIsVisible(false) 31 | } 32 | 33 | function handleOpenSignUp() { 34 | setSignInIsVisible(false) 35 | setSignUpIsVisible(true) 36 | } 37 | 38 | async function onSignIn(data: SignInFormProps) { 39 | const { token } = await signIn({ 40 | email: data.email, 41 | password: data.password, 42 | }) 43 | 44 | cookies.set('NaturaChallenge:Token', token) 45 | 46 | const user = await getAuthenticatedUser(token) 47 | 48 | setUser(user) 49 | 50 | setSignInIsVisible(false) 51 | } 52 | 53 | return ( 54 | 59 | 63 | 64 |
65 |
66 | 70 |
71 |

72 | Entrar 73 |

74 |
75 | 76 |
77 |
78 |
79 | 82 |
83 | 89 | {errors.email && ( 90 |

91 | {errors.email.message} 92 |

93 | )} 94 |
95 |
96 | 97 |
98 | 101 |
102 | 108 | {errors.password && ( 109 |

110 | {errors.password.message} 111 |

112 | )} 113 |
114 |
115 | 116 |
117 | 123 |
124 |
125 | 126 |

127 | Não tem uma conta?{' '} 128 | 132 | Criar conta 133 | 134 |

135 |
136 |
137 |
138 |
139 |
140 | ) 141 | } 142 | --------------------------------------------------------------------------------