kybernus 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/dist/cli/commands/ecommerce.d.ts +3 -0
  2. package/dist/cli/commands/ecommerce.d.ts.map +1 -0
  3. package/dist/cli/commands/ecommerce.js +164 -0
  4. package/dist/cli/commands/ecommerce.js.map +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/ecommerce/.env.example +10 -0
  9. package/templates/ecommerce/.github/workflows/ci.yml +102 -0
  10. package/templates/ecommerce/.github/workflows/deploy.yml +31 -0
  11. package/templates/ecommerce/.prettierrc +9 -0
  12. package/templates/ecommerce/Dockerfile +54 -0
  13. package/templates/ecommerce/README.md +295 -0
  14. package/templates/ecommerce/apps/api/.env.example +59 -0
  15. package/templates/ecommerce/apps/api/jest.config.ts +50 -0
  16. package/templates/ecommerce/apps/api/jest.integration.config.ts +45 -0
  17. package/templates/ecommerce/apps/api/package.json +59 -0
  18. package/templates/ecommerce/apps/api/prisma/migrations/20260306000137_init/migration.sql +184 -0
  19. package/templates/ecommerce/apps/api/prisma/migrations/migration_lock.toml +3 -0
  20. package/templates/ecommerce/apps/api/prisma/schema.prisma +181 -0
  21. package/templates/ecommerce/apps/api/prisma/seed.ts +159 -0
  22. package/templates/ecommerce/apps/api/src/__tests__/app.test.ts +39 -0
  23. package/templates/ecommerce/apps/api/src/__tests__/globalSetup.ts +34 -0
  24. package/templates/ecommerce/apps/api/src/__tests__/globalTeardown.ts +16 -0
  25. package/templates/ecommerce/apps/api/src/__tests__/setup.db.ts +18 -0
  26. package/templates/ecommerce/apps/api/src/__tests__/setup.env.ts +14 -0
  27. package/templates/ecommerce/apps/api/src/app.ts +133 -0
  28. package/templates/ecommerce/apps/api/src/application/admin/admin-user.service.ts +24 -0
  29. package/templates/ecommerce/apps/api/src/application/admin/dashboard.service.ts +102 -0
  30. package/templates/ecommerce/apps/api/src/application/auth/auth.service.ts +185 -0
  31. package/templates/ecommerce/apps/api/src/application/cart/cart.service.ts +151 -0
  32. package/templates/ecommerce/apps/api/src/application/cart/coupon.service.ts +51 -0
  33. package/templates/ecommerce/apps/api/src/application/catalog/catalog.service.ts +168 -0
  34. package/templates/ecommerce/apps/api/src/application/checkout/checkout.service.ts +114 -0
  35. package/templates/ecommerce/apps/api/src/application/orders/order.service.ts +93 -0
  36. package/templates/ecommerce/apps/api/src/application/ports/email.port.ts +3 -0
  37. package/templates/ecommerce/apps/api/src/application/ports/payment.port.ts +24 -0
  38. package/templates/ecommerce/apps/api/src/application/ports/shipping.port.ts +9 -0
  39. package/templates/ecommerce/apps/api/src/application/ports/storage.port.ts +3 -0
  40. package/templates/ecommerce/apps/api/src/application/ports/token-blacklist.port.ts +4 -0
  41. package/templates/ecommerce/apps/api/src/application/ports/token.port.ts +18 -0
  42. package/templates/ecommerce/apps/api/src/application/profile/profile.service.ts +76 -0
  43. package/templates/ecommerce/apps/api/src/domain/auth/user.entity.ts +109 -0
  44. package/templates/ecommerce/apps/api/src/domain/auth/user.repository.ts +11 -0
  45. package/templates/ecommerce/apps/api/src/domain/cart/cart.entity.ts +136 -0
  46. package/templates/ecommerce/apps/api/src/domain/cart/cart.repository.ts +8 -0
  47. package/templates/ecommerce/apps/api/src/domain/cart/coupon.entity.ts +58 -0
  48. package/templates/ecommerce/apps/api/src/domain/cart/coupon.repository.ts +10 -0
  49. package/templates/ecommerce/apps/api/src/domain/catalog/category.entity.ts +51 -0
  50. package/templates/ecommerce/apps/api/src/domain/catalog/category.repository.ts +10 -0
  51. package/templates/ecommerce/apps/api/src/domain/catalog/product.entity.ts +130 -0
  52. package/templates/ecommerce/apps/api/src/domain/catalog/product.repository.ts +28 -0
  53. package/templates/ecommerce/apps/api/src/domain/checkout/order.entity.ts +121 -0
  54. package/templates/ecommerce/apps/api/src/domain/checkout/order.repository.ts +11 -0
  55. package/templates/ecommerce/apps/api/src/domain/shared/AppError.ts +12 -0
  56. package/templates/ecommerce/apps/api/src/infrastructure/cache/redis.ts +16 -0
  57. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/admin.registry.ts +13 -0
  58. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/auth.registry.ts +34 -0
  59. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/cart.registry.ts +49 -0
  60. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/catalog.registry.ts +24 -0
  61. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/checkout.registry.ts +47 -0
  62. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/orders.registry.ts +6 -0
  63. package/templates/ecommerce/apps/api/src/infrastructure/config/registry/profile.registry.ts +4 -0
  64. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/cart.memory.repository.ts +33 -0
  65. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/category.memory.repository.ts +41 -0
  66. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/coupon.memory.repository.ts +55 -0
  67. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/order.memory.repository.ts +75 -0
  68. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/product.memory.repository.ts +100 -0
  69. package/templates/ecommerce/apps/api/src/infrastructure/persistence/in-memory/user.memory.repository.ts +54 -0
  70. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/auth/user.prisma.repository.ts +83 -0
  71. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/category.prisma.repository.ts +69 -0
  72. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/catalog/product.prisma.repository.ts +185 -0
  73. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma/checkout/order.prisma.repository.ts +149 -0
  74. package/templates/ecommerce/apps/api/src/infrastructure/persistence/prisma-client.ts +17 -0
  75. package/templates/ecommerce/apps/api/src/infrastructure/services/email/email.registry.ts +18 -0
  76. package/templates/ecommerce/apps/api/src/infrastructure/services/email/ethereal.email.service.ts +38 -0
  77. package/templates/ecommerce/apps/api/src/infrastructure/services/email/noop.email.service.ts +12 -0
  78. package/templates/ecommerce/apps/api/src/infrastructure/services/email/smtp.email.service.ts +36 -0
  79. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe-webhook.handler.ts +83 -0
  80. package/templates/ecommerce/apps/api/src/infrastructure/services/payment/stripe.adapter.ts +39 -0
  81. package/templates/ecommerce/apps/api/src/infrastructure/services/shipping/mock.shipping.service.ts +17 -0
  82. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/in-memory.storage.service.ts +11 -0
  83. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/local-disk.storage.service.ts +27 -0
  84. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/s3.storage.service.ts +52 -0
  85. package/templates/ecommerce/apps/api/src/infrastructure/services/storage/storage.registry.ts +19 -0
  86. package/templates/ecommerce/apps/api/src/infrastructure/services/token/redis.token.blacklist.ts +23 -0
  87. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.blacklist.ts +30 -0
  88. package/templates/ecommerce/apps/api/src/infrastructure/services/token/token.service.ts +136 -0
  89. package/templates/ecommerce/apps/api/src/modules/admin/__tests__/admin.routes.integration.test.ts +250 -0
  90. package/templates/ecommerce/apps/api/src/modules/admin/admin.controller.ts +116 -0
  91. package/templates/ecommerce/apps/api/src/modules/admin/admin.registry.ts +1 -0
  92. package/templates/ecommerce/apps/api/src/modules/admin/admin.routes.ts +21 -0
  93. package/templates/ecommerce/apps/api/src/modules/admin/admin.service.ts +1 -0
  94. package/templates/ecommerce/apps/api/src/modules/admin/admin.user.service.ts +1 -0
  95. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.logout.redis.test.ts +104 -0
  96. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.routes.integration.test.ts +211 -0
  97. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/auth.service.unit.test.ts +260 -0
  98. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/email.service.unit.test.ts +94 -0
  99. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/token.blacklist.redis.test.ts +65 -0
  100. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.entity.unit.test.ts +79 -0
  101. package/templates/ecommerce/apps/api/src/modules/auth/__tests__/user.prisma.repository.test.ts +138 -0
  102. package/templates/ecommerce/apps/api/src/modules/auth/auth.controller.ts +148 -0
  103. package/templates/ecommerce/apps/api/src/modules/auth/auth.registry.ts +1 -0
  104. package/templates/ecommerce/apps/api/src/modules/auth/auth.routes.ts +17 -0
  105. package/templates/ecommerce/apps/api/src/modules/auth/auth.service.ts +1 -0
  106. package/templates/ecommerce/apps/api/src/modules/auth/redis.token.blacklist.ts +1 -0
  107. package/templates/ecommerce/apps/api/src/modules/auth/token.blacklist.ts +2 -0
  108. package/templates/ecommerce/apps/api/src/modules/auth/token.service.ts +2 -0
  109. package/templates/ecommerce/apps/api/src/modules/auth/user.entity.ts +1 -0
  110. package/templates/ecommerce/apps/api/src/modules/auth/user.prisma.repository.ts +1 -0
  111. package/templates/ecommerce/apps/api/src/modules/auth/user.repository.ts +2 -0
  112. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.entity.unit.test.ts +144 -0
  113. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.routes.integration.test.ts +242 -0
  114. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/cart.service.unit.test.ts +151 -0
  115. package/templates/ecommerce/apps/api/src/modules/cart/__tests__/coupon.admin.integration.test.ts +136 -0
  116. package/templates/ecommerce/apps/api/src/modules/cart/cart.controller.ts +94 -0
  117. package/templates/ecommerce/apps/api/src/modules/cart/cart.entity.ts +1 -0
  118. package/templates/ecommerce/apps/api/src/modules/cart/cart.registry.ts +1 -0
  119. package/templates/ecommerce/apps/api/src/modules/cart/cart.repository.ts +2 -0
  120. package/templates/ecommerce/apps/api/src/modules/cart/cart.routes.ts +17 -0
  121. package/templates/ecommerce/apps/api/src/modules/cart/cart.service.ts +1 -0
  122. package/templates/ecommerce/apps/api/src/modules/cart/coupon.entity.ts +1 -0
  123. package/templates/ecommerce/apps/api/src/modules/cart/coupon.repository.ts +2 -0
  124. package/templates/ecommerce/apps/api/src/modules/cart/coupon.service.ts +1 -0
  125. package/templates/ecommerce/apps/api/src/modules/cart/shipping.service.ts +2 -0
  126. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.routes.integration.test.ts +275 -0
  127. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/catalog.service.unit.test.ts +223 -0
  128. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.image.integration.test.ts +130 -0
  129. package/templates/ecommerce/apps/api/src/modules/catalog/__tests__/product.prisma.repository.test.ts +174 -0
  130. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.controller.ts +176 -0
  131. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.registry.ts +1 -0
  132. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.routes.ts +38 -0
  133. package/templates/ecommerce/apps/api/src/modules/catalog/catalog.service.ts +1 -0
  134. package/templates/ecommerce/apps/api/src/modules/catalog/category.entity.ts +1 -0
  135. package/templates/ecommerce/apps/api/src/modules/catalog/category.prisma.repository.ts +1 -0
  136. package/templates/ecommerce/apps/api/src/modules/catalog/category.repository.ts +2 -0
  137. package/templates/ecommerce/apps/api/src/modules/catalog/product.entity.ts +1 -0
  138. package/templates/ecommerce/apps/api/src/modules/catalog/product.prisma.repository.ts +1 -0
  139. package/templates/ecommerce/apps/api/src/modules/catalog/product.repository.ts +2 -0
  140. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.routes.integration.test.ts +163 -0
  141. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/checkout.service.unit.test.ts +191 -0
  142. package/templates/ecommerce/apps/api/src/modules/checkout/__tests__/order.prisma.repository.test.ts +150 -0
  143. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.controller.ts +59 -0
  144. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.registry.ts +1 -0
  145. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.routes.ts +18 -0
  146. package/templates/ecommerce/apps/api/src/modules/checkout/checkout.service.ts +1 -0
  147. package/templates/ecommerce/apps/api/src/modules/checkout/order.entity.ts +1 -0
  148. package/templates/ecommerce/apps/api/src/modules/checkout/order.prisma.repository.ts +1 -0
  149. package/templates/ecommerce/apps/api/src/modules/checkout/order.repository.ts +2 -0
  150. package/templates/ecommerce/apps/api/src/modules/checkout/tax.service.ts +9 -0
  151. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.entity.unit.test.ts +68 -0
  152. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.routes.integration.test.ts +254 -0
  153. package/templates/ecommerce/apps/api/src/modules/orders/__tests__/order.service.email.unit.test.ts +142 -0
  154. package/templates/ecommerce/apps/api/src/modules/orders/order.controller.ts +96 -0
  155. package/templates/ecommerce/apps/api/src/modules/orders/order.registry.ts +1 -0
  156. package/templates/ecommerce/apps/api/src/modules/orders/order.routes.ts +17 -0
  157. package/templates/ecommerce/apps/api/src/modules/orders/order.service.ts +1 -0
  158. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe-webhook.unit.test.ts +330 -0
  159. package/templates/ecommerce/apps/api/src/modules/payment/__tests__/stripe.adapter.unit.test.ts +84 -0
  160. package/templates/ecommerce/apps/api/src/modules/payment/adapters/stripe.adapter.ts +1 -0
  161. package/templates/ecommerce/apps/api/src/modules/payment/payment.port.ts +1 -0
  162. package/templates/ecommerce/apps/api/src/modules/payment/stripe-webhook.handler.ts +1 -0
  163. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.routes.integration.test.ts +180 -0
  164. package/templates/ecommerce/apps/api/src/modules/profile/__tests__/profile.service.unit.test.ts +187 -0
  165. package/templates/ecommerce/apps/api/src/modules/profile/profile.controller.ts +92 -0
  166. package/templates/ecommerce/apps/api/src/modules/profile/profile.registry.ts +1 -0
  167. package/templates/ecommerce/apps/api/src/modules/profile/profile.routes.ts +14 -0
  168. package/templates/ecommerce/apps/api/src/modules/profile/profile.service.ts +1 -0
  169. package/templates/ecommerce/apps/api/src/presentation/middlewares/authenticate.ts +37 -0
  170. package/templates/ecommerce/apps/api/src/presentation/middlewares/authorize.ts +23 -0
  171. package/templates/ecommerce/apps/api/src/presentation/middlewares/errorHandler.ts +48 -0
  172. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.controller.ts +116 -0
  173. package/templates/ecommerce/apps/api/src/presentation/modules/admin/admin.routes.ts +21 -0
  174. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.controller.ts +147 -0
  175. package/templates/ecommerce/apps/api/src/presentation/modules/auth/auth.routes.ts +17 -0
  176. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.controller.ts +94 -0
  177. package/templates/ecommerce/apps/api/src/presentation/modules/cart/cart.routes.ts +17 -0
  178. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.controller.ts +176 -0
  179. package/templates/ecommerce/apps/api/src/presentation/modules/catalog/catalog.routes.ts +38 -0
  180. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.controller.ts +59 -0
  181. package/templates/ecommerce/apps/api/src/presentation/modules/checkout/checkout.routes.ts +18 -0
  182. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.controller.ts +96 -0
  183. package/templates/ecommerce/apps/api/src/presentation/modules/orders/order.routes.ts +17 -0
  184. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.controller.ts +92 -0
  185. package/templates/ecommerce/apps/api/src/presentation/modules/profile/profile.routes.ts +14 -0
  186. package/templates/ecommerce/apps/api/src/presentation/validators/uuidParam.ts +20 -0
  187. package/templates/ecommerce/apps/api/src/server.ts +47 -0
  188. package/templates/ecommerce/apps/api/src/shared/__tests__/uuid.validation.test.ts +111 -0
  189. package/templates/ecommerce/apps/api/src/shared/errors/AppError.ts +1 -0
  190. package/templates/ecommerce/apps/api/src/shared/infra/email/EtherealEmailService.ts +1 -0
  191. package/templates/ecommerce/apps/api/src/shared/infra/email/IEmailService.ts +1 -0
  192. package/templates/ecommerce/apps/api/src/shared/infra/email/NoopEmailService.ts +1 -0
  193. package/templates/ecommerce/apps/api/src/shared/infra/email/SmtpEmailService.ts +1 -0
  194. package/templates/ecommerce/apps/api/src/shared/infra/email/__tests__/ethereal.email.integration.test.ts +32 -0
  195. package/templates/ecommerce/apps/api/src/shared/infra/email/email.registry.ts +1 -0
  196. package/templates/ecommerce/apps/api/src/shared/infra/prisma.ts +1 -0
  197. package/templates/ecommerce/apps/api/src/shared/infra/redis.ts +1 -0
  198. package/templates/ecommerce/apps/api/src/shared/infra/storage/IStorageService.ts +1 -0
  199. package/templates/ecommerce/apps/api/src/shared/infra/storage/InMemoryStorageService.ts +1 -0
  200. package/templates/ecommerce/apps/api/src/shared/infra/storage/LocalDiskStorageService.ts +1 -0
  201. package/templates/ecommerce/apps/api/src/shared/infra/storage/S3StorageService.ts +1 -0
  202. package/templates/ecommerce/apps/api/src/shared/infra/storage/__tests__/s3.storage.unit.test.ts +73 -0
  203. package/templates/ecommerce/apps/api/src/shared/infra/storage/storage.registry.ts +1 -0
  204. package/templates/ecommerce/apps/api/src/shared/middlewares/authenticate.ts +1 -0
  205. package/templates/ecommerce/apps/api/src/shared/middlewares/authorize.ts +1 -0
  206. package/templates/ecommerce/apps/api/src/shared/middlewares/errorHandler.ts +1 -0
  207. package/templates/ecommerce/apps/api/src/shared/validators/uuidParam.ts +1 -0
  208. package/templates/ecommerce/apps/api/tsconfig.json +15 -0
  209. package/templates/ecommerce/apps/web/.env.example +8 -0
  210. package/templates/ecommerce/apps/web/index.html +19 -0
  211. package/templates/ecommerce/apps/web/jest.config.ts +45 -0
  212. package/templates/ecommerce/apps/web/package.json +38 -0
  213. package/templates/ecommerce/apps/web/src/App.tsx +133 -0
  214. package/templates/ecommerce/apps/web/src/__mocks__/fileMock.ts +1 -0
  215. package/templates/ecommerce/apps/web/src/__mocks__/styleMock.ts +1 -0
  216. package/templates/ecommerce/apps/web/src/index.css +159 -0
  217. package/templates/ecommerce/apps/web/src/main.tsx +13 -0
  218. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/CouponsAdminPage.test.tsx +134 -0
  219. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/DashboardPage.test.tsx +65 -0
  220. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/OrdersAdminPage.test.tsx +79 -0
  221. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/ProductsAdminPage.test.tsx +84 -0
  222. package/templates/ecommerce/apps/web/src/modules/admin/__tests__/UsersAdminPage.test.tsx +85 -0
  223. package/templates/ecommerce/apps/web/src/modules/admin/pages/CouponsAdminPage.tsx +179 -0
  224. package/templates/ecommerce/apps/web/src/modules/admin/pages/DashboardPage.tsx +58 -0
  225. package/templates/ecommerce/apps/web/src/modules/admin/pages/OrdersAdminPage.tsx +178 -0
  226. package/templates/ecommerce/apps/web/src/modules/admin/pages/ProductsAdminPage.tsx +444 -0
  227. package/templates/ecommerce/apps/web/src/modules/admin/pages/UsersAdminPage.tsx +87 -0
  228. package/templates/ecommerce/apps/web/src/modules/auth/LoginForm.tsx +91 -0
  229. package/templates/ecommerce/apps/web/src/modules/auth/RegisterForm.tsx +109 -0
  230. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ForgotPasswordPage.test.tsx +42 -0
  231. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/LoginForm.test.tsx +76 -0
  232. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/RegisterForm.test.tsx +62 -0
  233. package/templates/ecommerce/apps/web/src/modules/auth/__tests__/ResetPasswordPage.test.tsx +66 -0
  234. package/templates/ecommerce/apps/web/src/modules/auth/pages/ForgotPasswordPage.tsx +100 -0
  235. package/templates/ecommerce/apps/web/src/modules/auth/pages/LoginPage.tsx +39 -0
  236. package/templates/ecommerce/apps/web/src/modules/auth/pages/RegisterPage.tsx +39 -0
  237. package/templates/ecommerce/apps/web/src/modules/auth/pages/ResetPasswordPage.tsx +110 -0
  238. package/templates/ecommerce/apps/web/src/modules/auth/useAuthStore.ts +141 -0
  239. package/templates/ecommerce/apps/web/src/modules/cart/__tests__/CartPage.test.tsx +111 -0
  240. package/templates/ecommerce/apps/web/src/modules/cart/pages/CartPage.tsx +313 -0
  241. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductCard.test.tsx +59 -0
  242. package/templates/ecommerce/apps/web/src/modules/catalog/__tests__/ProductFilters.test.tsx +56 -0
  243. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductCard.tsx +78 -0
  244. package/templates/ecommerce/apps/web/src/modules/catalog/components/ProductFilters.tsx +104 -0
  245. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductDetailPage.tsx +179 -0
  246. package/templates/ecommerce/apps/web/src/modules/catalog/pages/ProductListPage.tsx +100 -0
  247. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/CheckoutPage.test.tsx +159 -0
  248. package/templates/ecommerce/apps/web/src/modules/checkout/__tests__/StripePaymentForm.test.tsx +79 -0
  249. package/templates/ecommerce/apps/web/src/modules/checkout/components/StripePaymentForm.tsx +55 -0
  250. package/templates/ecommerce/apps/web/src/modules/checkout/hooks/useCheckout.ts +56 -0
  251. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutPage.tsx +344 -0
  252. package/templates/ecommerce/apps/web/src/modules/checkout/pages/CheckoutSuccessPage.tsx +12 -0
  253. package/templates/ecommerce/apps/web/src/modules/legal/pages/PrivacyPolicyPage.tsx +207 -0
  254. package/templates/ecommerce/apps/web/src/modules/legal/pages/TermsOfServicePage.tsx +175 -0
  255. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderDetailPage.test.tsx +75 -0
  256. package/templates/ecommerce/apps/web/src/modules/orders/__tests__/OrderHistoryPage.test.tsx +87 -0
  257. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderDetailPage.tsx +73 -0
  258. package/templates/ecommerce/apps/web/src/modules/orders/pages/OrderHistoryPage.tsx +97 -0
  259. package/templates/ecommerce/apps/web/src/modules/profile/__tests__/ProfilePage.test.tsx +150 -0
  260. package/templates/ecommerce/apps/web/src/modules/profile/pages/ProfilePage.tsx +275 -0
  261. package/templates/ecommerce/apps/web/src/setupTests.ts +10 -0
  262. package/templates/ecommerce/apps/web/src/shared/components/CookieConsent.tsx +108 -0
  263. package/templates/ecommerce/apps/web/src/shared/components/ErrorBoundary.tsx +112 -0
  264. package/templates/ecommerce/apps/web/src/shared/components/Layout.tsx +143 -0
  265. package/templates/ecommerce/apps/web/src/shared/components/ProtectedRoute.tsx +21 -0
  266. package/templates/ecommerce/apps/web/src/shared/config/siteConfig.ts +57 -0
  267. package/templates/ecommerce/apps/web/src/shared/hooks/usePageTitle.ts +16 -0
  268. package/templates/ecommerce/apps/web/src/shared/lib/apiFetch.ts +16 -0
  269. package/templates/ecommerce/apps/web/src/shared/pages/NotFoundPage.tsx +42 -0
  270. package/templates/ecommerce/apps/web/src/shared/theme/ThemeProvider.tsx +45 -0
  271. package/templates/ecommerce/apps/web/src/shared/theme/__tests__/ThemeProvider.test.tsx +78 -0
  272. package/templates/ecommerce/apps/web/src/shared/theme/createTheme.ts +58 -0
  273. package/templates/ecommerce/apps/web/src/shared/theme/tokens.ts +81 -0
  274. package/templates/ecommerce/apps/web/src/vite-env.d.ts +1 -0
  275. package/templates/ecommerce/apps/web/tsconfig.jest.json +12 -0
  276. package/templates/ecommerce/apps/web/tsconfig.json +25 -0
  277. package/templates/ecommerce/apps/web/tsconfig.node.json +11 -0
  278. package/templates/ecommerce/apps/web/vite.config.ts +30 -0
  279. package/templates/ecommerce/docker-compose.yml +85 -0
  280. package/templates/ecommerce/package-lock.json +11255 -0
  281. package/templates/ecommerce/package.json +27 -0
  282. package/templates/ecommerce/packages/shared-types/package.json +13 -0
  283. package/templates/ecommerce/packages/shared-types/src/index.ts +3 -0
  284. package/templates/ecommerce/packages/shared-types/src/theme.ts +44 -0
  285. package/templates/ecommerce/packages/shared-types/tsconfig.json +11 -0
  286. package/templates/ecommerce/scripts/customize.sh +201 -0
  287. package/templates/ecommerce/tsconfig.json +14 -0
@@ -0,0 +1,444 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { apiFetch } from '../../../shared/lib/apiFetch';
3
+ import { useAuthStore } from '../../auth/useAuthStore';
4
+ import { usePageTitle } from '../../../shared/hooks/usePageTitle';
5
+
6
+ interface ProductVariant {
7
+ id?: string;
8
+ sku: string;
9
+ size?: string;
10
+ color?: string;
11
+ stock: number;
12
+ price?: number | null;
13
+ }
14
+
15
+ interface Product {
16
+ id: string;
17
+ name: string;
18
+ slug: string;
19
+ description?: string;
20
+ price: number;
21
+ categoryId?: string;
22
+ images: string[];
23
+ status: string;
24
+ variants: ProductVariant[];
25
+ }
26
+
27
+ interface Category {
28
+ id: string;
29
+ name: string;
30
+ slug: string;
31
+ }
32
+
33
+ function formatBRL(value: number) {
34
+ return value.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
35
+ }
36
+
37
+ // ── Blank variant template ────────────────────────────────────────────────────
38
+ const blankVariant = (): ProductVariant => ({ sku: '', size: '', color: '', stock: 0, price: null });
39
+
40
+ // ── Shared styles ─────────────────────────────────────────────────────────────
41
+ const thStyle: React.CSSProperties = {
42
+ textAlign: 'left',
43
+ padding: '0.75rem 1rem',
44
+ fontSize: '0.75rem',
45
+ fontWeight: 600,
46
+ color: '#6b7280',
47
+ textTransform: 'uppercase',
48
+ letterSpacing: '0.05em',
49
+ };
50
+ const tdStyle: React.CSSProperties = { padding: '0.75rem 1rem', fontSize: '0.875rem', color: '#111827', verticalAlign: 'middle' };
51
+ const inputStyle: React.CSSProperties = { width: '100%', padding: '0.5rem 0.75rem', border: '1px solid #d1d5db', borderRadius: '6px', fontSize: '0.875rem', boxSizing: 'border-box', fontFamily: 'inherit' };
52
+ const labelStyle: React.CSSProperties = { display: 'block', fontSize: '0.8rem', fontWeight: 600, color: '#374151', marginBottom: '0.25rem' };
53
+ const fieldStyle: React.CSSProperties = { marginBottom: '0.875rem' };
54
+ const primaryBtn: React.CSSProperties = { background: '#6366f1', color: '#fff', border: 'none', borderRadius: '6px', padding: '0.5rem 1.25rem', fontWeight: 600, cursor: 'pointer', fontSize: '0.875rem' };
55
+ const secondaryBtn: React.CSSProperties = { background: 'transparent', border: '1px solid #d1d5db', color: '#374151', borderRadius: '6px', padding: '0.5rem 1.25rem', fontWeight: 500, cursor: 'pointer', fontSize: '0.875rem' };
56
+ const dangerBtn: React.CSSProperties = { background: 'transparent', border: '1px solid #fca5a5', color: '#dc2626', borderRadius: '6px', padding: '0.4rem 0.875rem', fontWeight: 500, cursor: 'pointer', fontSize: '0.8rem' };
57
+
58
+ // ── Modal overlay ─────────────────────────────────────────────────────────────
59
+ function Modal({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) {
60
+ return (
61
+ <div
62
+ role="dialog"
63
+ aria-modal="true"
64
+ aria-label={title}
65
+ style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1rem' }}
66
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
67
+ >
68
+ <div style={{ background: '#fff', borderRadius: '12px', width: '100%', maxWidth: 640, maxHeight: '90vh', overflowY: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.2)' }}>
69
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1.25rem 1.5rem', borderBottom: '1px solid #f3f4f6' }}>
70
+ <h2 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 700 }}>{title}</h2>
71
+ <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.25rem', color: '#9ca3af', lineHeight: 1 }}>✕</button>
72
+ </div>
73
+ <div style={{ padding: '1.5rem' }}>{children}</div>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ // ── Variant row editor ─────────────────────────────────────────────────────────
80
+ function VariantRow({ variant, index, onChange, onRemove, canRemove }: {
81
+ variant: ProductVariant;
82
+ index: number;
83
+ onChange: (i: number, v: ProductVariant) => void;
84
+ onRemove: (i: number) => void;
85
+ canRemove: boolean;
86
+ }) {
87
+ const u = (field: keyof ProductVariant, val: string) =>
88
+ onChange(index, { ...variant, [field]: field === 'stock' ? Number(val) : field === 'price' ? (val === '' ? null : Number(val)) : val });
89
+
90
+ return (
91
+ <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr auto', gap: '0.5rem', alignItems: 'center', marginBottom: '0.5rem' }}>
92
+ <input placeholder="SKU *" value={variant.sku} onChange={(e) => u('sku', e.target.value)} style={inputStyle} required />
93
+ <input placeholder="Tamanho" value={variant.size ?? ''} onChange={(e) => u('size', e.target.value)} style={inputStyle} />
94
+ <input placeholder="Cor" value={variant.color ?? ''} onChange={(e) => u('color', e.target.value)} style={inputStyle} />
95
+ <input placeholder="Estoque" type="number" min="0" value={variant.stock} onChange={(e) => u('stock', e.target.value)} style={inputStyle} />
96
+ <input placeholder="Preço" type="number" min="0" step="0.01" value={variant.price ?? ''} onChange={(e) => u('price', e.target.value)} style={inputStyle} />
97
+ <button type="button" onClick={() => onRemove(index)} disabled={!canRemove} style={{ ...dangerBtn, padding: '0.4rem 0.6rem', opacity: canRemove ? 1 : 0.3 }}>✕</button>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ // ── Product form (shared between create & edit) ───────────────────────────────
103
+ interface ProductFormProps {
104
+ initial?: Partial<Product>;
105
+ categories: Category[];
106
+ onSave: (data: Omit<Product, 'id' | 'slug' | 'status' | 'images'>) => Promise<void>; onCancel: () => void;
107
+ saving: boolean;
108
+ formError: string | null;
109
+ }
110
+
111
+ function ProductForm({ initial, categories, onSave, onCancel, saving, formError }: ProductFormProps) {
112
+ const [name, setName] = useState(initial?.name ?? '');
113
+ const [description, setDescription] = useState(initial?.description ?? '');
114
+ const [price, setPrice] = useState(String(initial?.price ?? ''));
115
+ const [categoryId, setCategoryId] = useState(initial?.categoryId ?? '');
116
+ const [variants, setVariants] = useState<ProductVariant[]>(
117
+ initial?.variants?.length ? initial.variants.map((v) => ({ sku: v.sku, size: v.size ?? '', color: v.color ?? '', stock: v.stock, price: v.price ?? null })) : [blankVariant()],
118
+ );
119
+
120
+ function handleVariantChange(i: number, v: ProductVariant) {
121
+ setVariants((prev) => prev.map((x, idx) => (idx === i ? v : x)));
122
+ }
123
+ function addVariant() { setVariants((prev) => [...prev, blankVariant()]); }
124
+ function removeVariant(i: number) { setVariants((prev) => prev.filter((_, idx) => idx !== i)); }
125
+
126
+ async function handleSubmit(e: React.FormEvent) {
127
+ e.preventDefault();
128
+ await onSave({ name, description: description || undefined, price: Number(price), categoryId: categoryId || undefined, variants });
129
+ }
130
+
131
+ return (
132
+ <form onSubmit={handleSubmit}>
133
+ {formError && <p role="alert" style={{ color: '#dc2626', marginBottom: '1rem', padding: '0.625rem 0.75rem', background: '#fef2f2', borderRadius: '6px', fontSize: '0.875rem' }}>{formError}</p>}
134
+
135
+ <div style={fieldStyle}>
136
+ <label style={labelStyle} htmlFor="pf-name">Nome *</label>
137
+ <input id="pf-name" style={inputStyle} value={name} onChange={(e) => setName(e.target.value)} required placeholder="Camiseta Básica" />
138
+ </div>
139
+
140
+ <div style={fieldStyle}>
141
+ <label style={labelStyle} htmlFor="pf-desc">Descrição</label>
142
+ <textarea id="pf-desc" style={{ ...inputStyle, height: 72, resize: 'vertical' }} value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Descrição do produto..." />
143
+ </div>
144
+
145
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginBottom: '0.875rem' }}>
146
+ <div>
147
+ <label style={labelStyle} htmlFor="pf-price">Preço base (R$) *</label>
148
+ <input id="pf-price" type="number" min="0" step="0.01" style={inputStyle} value={price} onChange={(e) => setPrice(e.target.value)} required placeholder="49.90" />
149
+ </div>
150
+ <div>
151
+ <label style={labelStyle} htmlFor="pf-cat">Categoria</label>
152
+ <select id="pf-cat" style={inputStyle} value={categoryId} onChange={(e) => setCategoryId(e.target.value)}>
153
+ <option value="">— sem categoria —</option>
154
+ {categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)}
155
+ </select>
156
+ </div>
157
+ </div>
158
+
159
+ <div style={{ borderTop: '1px solid #f3f4f6', paddingTop: '1rem', marginBottom: '0.5rem' }}>
160
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
161
+ <span style={{ fontWeight: 600, fontSize: '0.875rem', color: '#374151' }}>Variações *</span>
162
+ <button type="button" onClick={addVariant} style={{ ...secondaryBtn, padding: '0.25rem 0.75rem', fontSize: '0.8rem' }}>+ Adicionar</button>
163
+ </div>
164
+ <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr 1fr 1fr auto', gap: '0.5rem', marginBottom: '0.25rem' }}>
165
+ {['SKU', 'Tamanho', 'Cor', 'Estoque', 'Preço'].map((h) => <span key={h} style={{ fontSize: '0.7rem', fontWeight: 600, color: '#9ca3af', textTransform: 'uppercase' }}>{h}</span>)}
166
+ <span />
167
+ </div>
168
+ {variants.map((v, i) => (
169
+ <VariantRow key={i} variant={v} index={i} onChange={handleVariantChange} onRemove={removeVariant} canRemove={variants.length > 1} />
170
+ ))}
171
+ </div>
172
+
173
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1.25rem' }}>
174
+ <button type="button" onClick={onCancel} style={secondaryBtn}>Cancelar</button>
175
+ <button type="submit" disabled={saving} style={{ ...primaryBtn, opacity: saving ? 0.7 : 1 }}>{saving ? 'Salvando...' : 'Salvar'}</button>
176
+ </div>
177
+ </form>
178
+ );
179
+ }
180
+
181
+ // ── Main page ─────────────────────────────────────────────────────────────────
182
+ export function ProductsAdminPage() {
183
+ usePageTitle('Produtos (Admin)');
184
+ const [products, setProducts] = useState<Product[]>([]);
185
+ const [categories, setCategories] = useState<Category[]>([]);
186
+ const [loading, setLoading] = useState(true);
187
+ const [error, setError] = useState<string | null>(null);
188
+
189
+ // Modals
190
+ const [showCreate, setShowCreate] = useState(false);
191
+ const [editProduct, setEditProduct] = useState<Product | null>(null);
192
+ const [deleteTarget, setDeleteTarget] = useState<Product | null>(null);
193
+
194
+ // Saving / deleting state
195
+ const [saving, setSaving] = useState(false);
196
+ const [formError, setFormError] = useState<string | null>(null);
197
+ const [deleting, setDeleting] = useState(false);
198
+
199
+ // Image upload
200
+ const [uploadingId, setUploadingId] = useState<string | null>(null);
201
+ const fileInputRef = useRef<HTMLInputElement>(null);
202
+ const [activeUploadId, setActiveUploadId] = useState<string | null>(null);
203
+
204
+ function loadProducts() {
205
+ setLoading(true);
206
+ Promise.all([
207
+ apiFetch<{ items: Product[]; nextCursor: string | null }>('/api/products'),
208
+ apiFetch<Category[]>('/api/categories').catch(() => [] as Category[]),
209
+ ])
210
+ .then(([prodData, cats]) => {
211
+ setProducts(prodData.items);
212
+ setCategories(cats);
213
+ setLoading(false);
214
+ })
215
+ .catch(() => {
216
+ setError('Erro ao carregar produtos');
217
+ setLoading(false);
218
+ });
219
+ }
220
+
221
+ useEffect(() => { loadProducts(); }, []);
222
+
223
+ // ── Create ──────────────────────────────────────────────────────────────────
224
+ async function handleCreate(data: Omit<Product, 'id' | 'slug' | 'status' | 'images'>) {
225
+ setSaving(true);
226
+ setFormError(null);
227
+ try {
228
+ await apiFetch('/api/products', {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ ...data, images: [] }),
232
+ }); setShowCreate(false);
233
+ loadProducts();
234
+ } catch {
235
+ setFormError('Erro ao criar produto. Verifique os campos.');
236
+ } finally {
237
+ setSaving(false);
238
+ }
239
+ }
240
+
241
+ // ── Edit ────────────────────────────────────────────────────────────────────
242
+ async function handleEdit(data: Omit<Product, 'id' | 'slug' | 'status' | 'images'>) {
243
+ if (!editProduct) return;
244
+ setSaving(true);
245
+ setFormError(null);
246
+ try {
247
+ await apiFetch(`/api/products/${editProduct.id}`, {
248
+ method: 'PUT',
249
+ headers: { 'Content-Type': 'application/json' },
250
+ body: JSON.stringify({ name: data.name, description: data.description, price: data.price }),
251
+ });
252
+ setEditProduct(null);
253
+ loadProducts();
254
+ } catch {
255
+ setFormError('Erro ao atualizar produto.');
256
+ } finally {
257
+ setSaving(false);
258
+ }
259
+ }
260
+
261
+ // ── Delete ──────────────────────────────────────────────────────────────────
262
+ async function handleDelete() {
263
+ if (!deleteTarget) return;
264
+ setDeleting(true);
265
+ try {
266
+ await apiFetch(`/api/products/${deleteTarget.id}`, { method: 'DELETE' });
267
+ setDeleteTarget(null);
268
+ loadProducts();
269
+ } catch {
270
+ setError('Erro ao excluir produto.');
271
+ } finally {
272
+ setDeleting(false);
273
+ }
274
+ }
275
+
276
+ // ── Image upload ────────────────────────────────────────────────────────────
277
+ function handleUploadClick(productId: string) {
278
+ setActiveUploadId(productId);
279
+ fileInputRef.current?.click();
280
+ }
281
+
282
+ async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
283
+ const file = e.target.files?.[0];
284
+ if (!file || !activeUploadId) return;
285
+
286
+ setUploadingId(activeUploadId);
287
+ const formData = new FormData();
288
+ formData.append('image', file);
289
+
290
+ try {
291
+ const token = useAuthStore.getState().accessToken;
292
+ const res = await fetch(`/api/products/${activeUploadId}/image`, {
293
+ method: 'POST',
294
+ credentials: 'include',
295
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
296
+ body: formData,
297
+ });
298
+ if (!res.ok) {
299
+ const body = (await res.json()) as { error?: string };
300
+ setError(body.error ?? 'Erro ao fazer upload');
301
+ } else {
302
+ loadProducts();
303
+ }
304
+ } catch {
305
+ setError('Erro ao fazer upload');
306
+ } finally {
307
+ setUploadingId(null);
308
+ setActiveUploadId(null);
309
+ if (fileInputRef.current) fileInputRef.current.value = '';
310
+ }
311
+ }
312
+
313
+ if (loading) return <p style={{ color: '#6b7280', marginTop: '2rem' }}>Carregando...</p>;
314
+
315
+ return (
316
+ <main>
317
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
318
+ <h1 style={{ margin: 0 }}>Produtos</h1>
319
+ <button style={primaryBtn} onClick={() => { setFormError(null); setShowCreate(true); }}>+ Novo Produto</button>
320
+ </div>
321
+
322
+ {error && (
323
+ <p role="alert" style={{ color: '#ef4444', marginBottom: '1rem' }}>{error}</p>
324
+ )}
325
+
326
+ {/* Hidden file input for image upload */}
327
+ <input ref={fileInputRef} type="file" accept="image/jpeg,image/png,image/gif,image/webp" style={{ display: 'none' }} onChange={handleFileChange} />
328
+
329
+ {products.length === 0 && !loading && (
330
+ <div style={{ textAlign: 'center', padding: '3rem 1rem' }}>
331
+ <p style={{ color: '#6b7280', marginBottom: '1rem' }}>Nenhum produto cadastrado.</p>
332
+ <button
333
+ type="button"
334
+ onClick={() => { setFormError(null); setShowCreate(true); }}
335
+ style={primaryBtn}
336
+ >
337
+ + Cadastrar primeiro produto
338
+ </button>
339
+ </div>
340
+ )}
341
+
342
+ <div style={{ overflowX: 'auto' }}>
343
+ <table style={{ width: '100%', borderCollapse: 'collapse', background: '#fff', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.07)' }}>
344
+ <thead>
345
+ <tr style={{ background: '#f9fafb', borderBottom: '1px solid #e5e7eb' }}>
346
+ <th style={thStyle}>Imagem</th>
347
+ <th style={thStyle}>Nome</th>
348
+ <th style={thStyle}>Preço</th>
349
+ <th style={thStyle}>Variantes</th>
350
+ <th style={thStyle}>Status</th>
351
+ <th style={thStyle}>Ações</th>
352
+ </tr>
353
+ </thead>
354
+ <tbody>
355
+ {products.map((p) => (
356
+ <tr key={p.id} style={{ borderBottom: '1px solid #f3f4f6' }}>
357
+ <td style={tdStyle}>
358
+ {p.images[0] ? (
359
+ <img src={p.images[0]} alt={p.name} style={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 4 }} />
360
+ ) : (
361
+ <div style={{ width: 48, height: 48, background: '#e5e7eb', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.25rem' }}>📦</div>
362
+ )}
363
+ </td>
364
+ <td style={tdStyle}>{p.name}</td>
365
+ <td style={tdStyle}>{formatBRL(p.price)}</td>
366
+ <td style={tdStyle}>{p.variants.length}</td>
367
+ <td style={tdStyle}>
368
+ <span style={{ background: p.status === 'ACTIVE' ? '#d1fae5' : '#fee2e2', color: p.status === 'ACTIVE' ? '#065f46' : '#991b1b', padding: '2px 8px', borderRadius: 12, fontSize: '0.75rem', fontWeight: 600 }}>
369
+ {p.status}
370
+ </span>
371
+ </td>
372
+ <td style={tdStyle}>
373
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
374
+ <button
375
+ onClick={() => handleUploadClick(p.id)}
376
+ disabled={uploadingId === p.id}
377
+ style={{ ...secondaryBtn, padding: '0.3rem 0.7rem', fontSize: '0.78rem' }}
378
+ >
379
+ {uploadingId === p.id ? '...' : '📷'}
380
+ </button>
381
+ <button
382
+ onClick={() => { setFormError(null); setEditProduct(p); }}
383
+ style={{ ...secondaryBtn, padding: '0.3rem 0.7rem', fontSize: '0.78rem' }}
384
+ >
385
+ ✏️
386
+ </button>
387
+ <button
388
+ onClick={() => setDeleteTarget(p)}
389
+ style={{ ...dangerBtn, padding: '0.3rem 0.7rem', fontSize: '0.78rem' }}
390
+ >
391
+ 🗑
392
+ </button>
393
+ </div>
394
+ </td>
395
+ </tr>
396
+ ))}
397
+ </tbody>
398
+ </table>
399
+ </div>
400
+
401
+ {/* ── Create modal ──────────────────────────────────────────────────── */}
402
+ {showCreate && (
403
+ <Modal title="Novo Produto" onClose={() => setShowCreate(false)}>
404
+ <ProductForm
405
+ categories={categories}
406
+ onSave={handleCreate}
407
+ onCancel={() => setShowCreate(false)}
408
+ saving={saving}
409
+ formError={formError}
410
+ />
411
+ </Modal>
412
+ )}
413
+
414
+ {/* ── Edit modal ────────────────────────────────────────────────────── */}
415
+ {editProduct && (
416
+ <Modal title={`Editar: ${editProduct.name}`} onClose={() => setEditProduct(null)}>
417
+ <ProductForm
418
+ initial={editProduct}
419
+ categories={categories}
420
+ onSave={handleEdit}
421
+ onCancel={() => setEditProduct(null)}
422
+ saving={saving}
423
+ formError={formError}
424
+ />
425
+ </Modal>
426
+ )}
427
+
428
+ {/* ── Delete confirm modal ──────────────────────────────────────────── */}
429
+ {deleteTarget && (
430
+ <Modal title="Confirmar exclusão" onClose={() => setDeleteTarget(null)}>
431
+ <p style={{ marginTop: 0 }}>
432
+ Tem certeza que deseja excluir <strong>{deleteTarget.name}</strong>? Esta ação não pode ser desfeita.
433
+ </p>
434
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1.5rem' }}>
435
+ <button onClick={() => setDeleteTarget(null)} style={secondaryBtn}>Cancelar</button>
436
+ <button onClick={handleDelete} disabled={deleting} style={{ ...dangerBtn, padding: '0.5rem 1.25rem', opacity: deleting ? 0.7 : 1 }}>
437
+ {deleting ? 'Excluindo...' : 'Excluir'}
438
+ </button>
439
+ </div>
440
+ </Modal>
441
+ )}
442
+ </main>
443
+ );
444
+ }
@@ -0,0 +1,87 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { apiFetch } from '../../../shared/lib/apiFetch';
3
+ import { usePageTitle } from '../../../shared/hooks/usePageTitle';
4
+
5
+ type UserRole = 'ADMIN' | 'CUSTOMER';
6
+
7
+ interface User {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ role: UserRole;
12
+ createdAt: string;
13
+ }
14
+
15
+ interface UsersResponse {
16
+ items: User[];
17
+ nextCursor: string | null;
18
+ }
19
+
20
+ export function UsersAdminPage() {
21
+ usePageTitle('Usuários');
22
+ const [users, setUsers] = useState<User[]>([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+ const [roleFilter, setRoleFilter] = useState<string>('');
26
+
27
+ function loadUsers(role: string) {
28
+ setLoading(true);
29
+ setError(null);
30
+ const qs = role ? `?role=${role}` : '';
31
+ apiFetch<UsersResponse>(`/api/admin/users${qs}`)
32
+ .then((data) => {
33
+ setUsers(data.items);
34
+ setLoading(false);
35
+ })
36
+ .catch(() => {
37
+ setError('Erro ao carregar usuários');
38
+ setLoading(false);
39
+ });
40
+ }
41
+
42
+ useEffect(() => {
43
+ loadUsers('');
44
+ }, []);
45
+
46
+ function handleRoleChange(e: React.ChangeEvent<HTMLSelectElement>) {
47
+ const val = e.target.value;
48
+ setRoleFilter(val);
49
+ loadUsers(val);
50
+ }
51
+
52
+ if (loading) return <p>Carregando...</p>;
53
+ if (error) return <p role="alert">{error}</p>;
54
+
55
+ return (
56
+ <main>
57
+ <h1>Usuários</h1>
58
+ <select value={roleFilter} onChange={handleRoleChange} aria-label="Filtrar por role">
59
+ <option value="">Todos</option>
60
+ <option value="ADMIN">ADMIN</option>
61
+ <option value="CUSTOMER">CUSTOMER</option>
62
+ </select>
63
+ {users.length === 0 ? (
64
+ <p>Nenhum usuário encontrado</p>
65
+ ) : (
66
+ <table>
67
+ <thead>
68
+ <tr>
69
+ <th>Nome</th>
70
+ <th>Email</th>
71
+ <th>Role</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ {users.map((u) => (
76
+ <tr key={u.id}>
77
+ <td>{u.name}</td>
78
+ <td>{u.email}</td>
79
+ <td>{u.role}</td>
80
+ </tr>
81
+ ))}
82
+ </tbody>
83
+ </table>
84
+ )}
85
+ </main>
86
+ );
87
+ }
@@ -0,0 +1,91 @@
1
+ import { useState, FormEvent } from 'react';
2
+
3
+ export interface LoginFormData {
4
+ email: string;
5
+ password: string;
6
+ }
7
+
8
+ export interface LoginFormProps {
9
+ onSubmit: (data: LoginFormData) => void;
10
+ isLoading?: boolean;
11
+ errorMessage?: string;
12
+ }
13
+
14
+ interface FormErrors {
15
+ email?: string;
16
+ password?: string;
17
+ }
18
+
19
+ function validateEmail(email: string): boolean {
20
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
21
+ }
22
+
23
+ export function LoginForm({ onSubmit, isLoading = false, errorMessage }: LoginFormProps) {
24
+ const [email, setEmail] = useState('');
25
+ const [password, setPassword] = useState('');
26
+ const [errors, setErrors] = useState<FormErrors>({});
27
+
28
+ function validate(): FormErrors {
29
+ const errs: FormErrors = {};
30
+ if (!validateEmail(email)) errs.email = 'Email inválido';
31
+ if (!password) errs.password = 'Senha é obrigatória';
32
+ return errs;
33
+ }
34
+
35
+ function handleSubmit(e: FormEvent<HTMLFormElement>) {
36
+ e.preventDefault();
37
+ const errs = validate();
38
+ if (Object.keys(errs).length > 0) {
39
+ setErrors(errs);
40
+ return;
41
+ }
42
+ setErrors({});
43
+ onSubmit({ email, password });
44
+ }
45
+
46
+ const fieldStyle: React.CSSProperties = { marginBottom: '1.1rem' };
47
+ const errorStyle: React.CSSProperties = { display: 'block', marginTop: '0.3rem', fontSize: '0.8rem', color: '#ef4444' };
48
+
49
+ return (
50
+ <form onSubmit={handleSubmit} noValidate>
51
+ <div style={fieldStyle}>
52
+ <label htmlFor="login-email">Email</label>
53
+ <input
54
+ id="login-email"
55
+ type="email"
56
+ value={email}
57
+ onChange={(e) => setEmail(e.target.value)}
58
+ autoComplete="email"
59
+ placeholder="seu@email.com"
60
+ aria-invalid={!!errors.email}
61
+ />
62
+ {errors.email && <span style={errorStyle}>{errors.email}</span>}
63
+ </div>
64
+
65
+ <div style={{ ...fieldStyle, marginBottom: '1.5rem' }}>
66
+ <label htmlFor="login-password">Senha</label>
67
+ <input
68
+ id="login-password"
69
+ type="password"
70
+ value={password}
71
+ onChange={(e) => setPassword(e.target.value)}
72
+ autoComplete="current-password"
73
+ placeholder="••••••••"
74
+ aria-invalid={!!errors.password}
75
+ />
76
+ {errors.password && <span style={errorStyle}>{errors.password}</span>}
77
+ </div>
78
+
79
+ {errorMessage && (
80
+ <p role="alert" style={{ marginBottom: '1rem', padding: '0.625rem 0.75rem', background: '#fef2f2', color: '#dc2626', borderRadius: '0.5rem', fontSize: '0.875rem' }}>
81
+ {errorMessage}
82
+ </p>
83
+ )}
84
+
85
+ <button type="submit" disabled={isLoading}>
86
+ {isLoading && <span data-testid="spinner" aria-hidden="true" />}
87
+ Entrar
88
+ </button>
89
+ </form>
90
+ );
91
+ }