diff --git a/api/pom.xml b/api/pom.xml index 9ba2aa3..e6bb2a7 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,6 +21,12 @@ + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + org.springframework.boot spring-boot-starter-data-jpa diff --git a/api/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java b/api/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java index 0e9ff97..d1a172d 100644 --- a/api/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java +++ b/api/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java @@ -1,32 +1,51 @@ -package com.orderflow.ecommerce.config; + package com.orderflow.ecommerce.config; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; + import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + import org.springframework.security.config.http.SessionCreationPolicy; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + import org.springframework.security.crypto.password.PasswordEncoder; + import org.springframework.security.web.SecurityFilterChain; + import org.springframework.web.cors.CorsConfiguration; + import org.springframework.web.cors.CorsConfigurationSource; + import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -@Configuration -@EnableWebSecurity -public class SecurityConfig { + import java.util.Arrays; - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) // Desabilita CSRF (comum em APIs REST) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // API sem estado - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() // Por enquanto, libera tudo para você não se travar - ); + @Configuration + @EnableWebSecurity + public class SecurityConfig { - return http.build(); - } - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } -} \ No newline at end of file + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // Desabilita CSRF (comum em APIs REST) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) // Adiciona CORS + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // API sem estado + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // Por enquanto, libera tudo para você não se travar + ); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(Arrays.asList("*")); // Permite todas as origens (para desenvolvimento) + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d065089..c0bfa8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,6 +92,23 @@ services: - orderflow-network restart: unless-stopped + web-dev: + build: + context: ./web + dockerfile: Dockerfile.dev + container_name: orderflow-web-dev + ports: + - "5173:5173" + volumes: + - ./web:/app + - /app/node_modules + depends_on: + app: + condition: service_started + networks: + - orderflow-network + restart: unless-stopped + volumes: postgres_data: rabbitmq_data: diff --git a/web/Dockerfile.dev b/web/Dockerfile.dev new file mode 100644 index 0000000..32535d4 --- /dev/null +++ b/web/Dockerfile.dev @@ -0,0 +1,6 @@ +FROM node:22-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index bf516aa..e4bc71d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,10 +9,12 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.5", - "react-dom": "^19.2.5" + "react-dom": "^19.2.5", + "react-router-dom": "^7.14.2" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/postcss": "^4.2.4", "@tailwindcss/vite": "^4.2.4", "@types/node": "^24.12.2", "@types/react": "^19.2.14", @@ -22,12 +24,26 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", + "postcss": "^8.5.14", "tailwindcss": "^4.2.4", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -59,7 +75,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -269,6 +284,29 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -1074,6 +1112,20 @@ "node": ">= 20" } }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.4.tgz", + "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "postcss": "^8.5.6", + "tailwindcss": "4.2.4" + } + }, "node_modules/@tailwindcss/vite": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", @@ -1127,7 +1179,6 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1138,7 +1189,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1198,7 +1248,6 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -1429,7 +1478,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1520,7 +1568,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1563,6 +1610,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1670,7 +1730,6 @@ "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2586,7 +2645,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2595,9 +2653,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -2648,7 +2706,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2665,6 +2722,44 @@ "react": "^19.2.5" } }, + "node_modules/react-router": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rolldown": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", @@ -2722,6 +2817,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2833,7 +2934,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2920,7 +3020,6 @@ "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -3045,7 +3144,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/package.json b/web/package.json index 4ff02fc..a6055bd 100644 --- a/web/package.json +++ b/web/package.json @@ -10,11 +10,13 @@ "preview": "vite preview" }, "dependencies": { - "react": "^19.2.5", - "react-dom": "^19.2.5" + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router-dom": "^7.14.2" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/postcss": "^4.2.4", "@tailwindcss/vite": "^4.2.4", "@types/node": "^24.12.2", "@types/react": "^19.2.14", @@ -24,6 +26,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", + "postcss": "^8.5.14", "tailwindcss": "^4.2.4", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..db3c3d1 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +} \ No newline at end of file diff --git a/web/public/products/1.webp b/web/public/products/1.webp new file mode 100644 index 0000000..2800551 Binary files /dev/null and b/web/public/products/1.webp differ diff --git a/web/public/products/2.webp b/web/public/products/2.webp new file mode 100644 index 0000000..22dcd5b Binary files /dev/null and b/web/public/products/2.webp differ diff --git a/web/public/products/3.webp b/web/public/products/3.webp new file mode 100644 index 0000000..e52d20b Binary files /dev/null and b/web/public/products/3.webp differ diff --git a/web/public/products/4.webp b/web/public/products/4.webp new file mode 100644 index 0000000..20884d2 Binary files /dev/null and b/web/public/products/4.webp differ diff --git a/web/src/App.tsx b/web/src/App.tsx index ac122e0..f8eb619 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,56 +1,19 @@ -import { useCallback, useState } from 'react' - -import { apiGet, getApiBaseUrl } from './lib/api' - -function App() { - const [result, setResult] = useState('') - const [loading, setLoading] = useState(false) - - const tryPing = useCallback(async () => { - setLoading(true) - setResult('') - try { - const data = await apiGet<{ status?: string }>('/test/ping') - setResult( - `OK: ${JSON.stringify(data)}`, - ) - } catch (e) { - setResult(e instanceof Error ? e.message : 'Erro desconhecido') - } finally { - setLoading(false) - } - }, []) - - return ( -
-
-
-

- OrderFlow Web -

-

- Boilerplate React + TypeScript + Tailwind. Base da API:{' '} - - {getApiBaseUrl()} - -

-
- - {result ? ( -

- {result} -

- ) : null} -
-
+import { BrowserRouter, Routes, Route } from "react-router-dom" +import Navbar from "./components/Navbar" +import HomePage from "./app/HomePage/page" +import CheckoutPage from "./app/CheckoutPage/page" +import EmailConfirmPage from "./app/EmailConfirmPage/page" + +export default function App() { + return ( + + + + } /> + } /> + } /> + + ) } - -export default App + \ No newline at end of file diff --git a/web/src/app/HomePage/page.tsx b/web/src/app/HomePage/page.tsx new file mode 100644 index 0000000..648b839 --- /dev/null +++ b/web/src/app/HomePage/page.tsx @@ -0,0 +1,87 @@ +import { useState } from "react"; +import ProductCard from "../../components/ProductCard"; +import ProductCardSkeleton from "../../components/ProductCardSkeleton"; +import useProducts from "../../hooks/api/useProducts"; + +export default function Home() { + const { products, loading, error } = useProducts(); + const [search, setSearch] = useState(""); + + const filtered = products.filter((p) => + p.name.toLowerCase().includes(search.toLowerCase()), + ); + + return ( +
+ {loading ? ( +
+ {/* Busca */} +
+ + {/* Título */} +
+ + {/* Quantidade */} +
+ + {/* Cards */} +
+ {Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+
+ ) : ( + <> + {!error && ( + <> + {/* Busca */} + setSearch(e.target.value)} + /> + + {/* Título */} +

Produtos

+ + {/* Quantidade */} +

+ {filtered.length} itens +

+ + )} + + {/* ERRO */} + {error ? ( +
+

Produtos

+

{error}

+
+ {Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+
+ ) : ( + <> + {/* Nenhum produto */} + {filtered.length === 0 &&

Nenhum produto encontrado.

} + + {/* LISTA */} + {filtered.length > 0 && ( +
+ {filtered.map((product) => ( + + ))} +
+ )} + + )} + + )} +
+ ); +} \ No newline at end of file diff --git a/web/src/components/Navbar/index.tsx b/web/src/components/Navbar/index.tsx new file mode 100644 index 0000000..1abdd2d --- /dev/null +++ b/web/src/components/Navbar/index.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; + +export default function Navbar() { + // Controla se o menu mobile está aberto ou fechado + const [isOpen, setIsOpen] = useState(false); + + return ( + + ); +} \ No newline at end of file diff --git a/web/src/components/ProductCard/index.tsx b/web/src/components/ProductCard/index.tsx new file mode 100644 index 0000000..77aea29 --- /dev/null +++ b/web/src/components/ProductCard/index.tsx @@ -0,0 +1,46 @@ + type Category = { + id: number; + name: string; +}; + + type Product = { + id: number; + name: string; + category?: Category; + price: number; + }; + + type Props = { + product: Product; + }; + + export default function ProductCard({ product }: Props) { + return ( +
+ {/* Imagem */} + {product.name} + + {/* Categoria */} + {product.category && ( + + {product.category.name} + + )} + + {/* Nome */} +

{product.name}

+ + {/* Preço */} +

R$ {product.price.toFixed(2)}

+ + {/* Botão */} + +
+ ); + } \ No newline at end of file diff --git a/web/src/components/ProductCardSkeleton/index.tsx b/web/src/components/ProductCardSkeleton/index.tsx new file mode 100644 index 0000000..abe41d6 --- /dev/null +++ b/web/src/components/ProductCardSkeleton/index.tsx @@ -0,0 +1,20 @@ +export default function ProductCardSkeleton() { + return ( +
+ {/* Imagem */} +
+ + {/* Categoria */} +
+ + {/* Nome */} +
+ + {/* Preço */} +
+ + {/* Botão */} +
+
+ ); +} \ No newline at end of file diff --git a/web/src/hooks/api/useProducts.ts b/web/src/hooks/api/useProducts.ts new file mode 100644 index 0000000..a68cd11 --- /dev/null +++ b/web/src/hooks/api/useProducts.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; + + type Category = { + id: number; + name: string; + }; + + type Product = { + id: number; + name: string; + category?: Category; + price: number; + }; + + export default function useProducts() { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchProducts() { + try { + setLoading(true); + + console.log("Buscando produtos..."); + + const res = await fetch("http://localhost:8080/products"); + + if (!res.ok) throw new Error("Erro ao buscar produtos"); + + const data: Product[] = await res.json(); + + console.log("Produtos recebidos:", data); + + setProducts(data); + } catch (err: unknown) { + if (err instanceof Error) { + setError("Não foi possível carregar os produtos"); + } else { + setError("Erro desconhecido"); + } + } finally { + setLoading(false); + } + } + + fetchProducts(); +}, []); + + return { + products, + loading, + error, + }; +} \ No newline at end of file diff --git a/web/src/index.css b/web/src/index.css index 03d224a..066c6ab 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,5 +1,38 @@ @import "tailwindcss"; +@theme { + --color-text: #141414; + --color-textSecondary: #737373; + --color-textHint: #a6a6a6; + --color-surface: #ffffff; + --color-bg: #f7f7f7; + --color-border: #e0e0e0; + --color-primary: #378add; + --color-primaryLight: #e6f1fb; + --color-primaryDark: #185fa5; + --color-successText: #3b6d11; + --color-errorLight: #fcebeb; + --color-errorText: #a32d2d; + --color-skeleton: #e8e8e8; +} + +:root { + --color-text: #141414; + --color-textSecondary: #737373; + --color-textHint: #a6a6a6; + --color-surface: #ffffff; + --color-bg: #f7f7f7; + --color-border: #e0e0e0; + --color-primary: #378add; + --color-primaryLight: #e6f1fb; + --color-primaryDark: #185fa5; + --color-successText: #3b6d11; + --color-errorLight: #fcebeb; + --color-errorText: #a32d2d; + --color-skeleton: #e8e8e8; +} + body { margin: 0; + background-color: var(--color-bg); }