@devxcrew/react-ui 0.0.0-stage → 0.1.7

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 (326) hide show
  1. package/README.md +85 -2
  2. package/package.json +105 -4
  3. package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
  4. package/src/blocks/agent-chat-workspace/index.ts +2 -0
  5. package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
  6. package/src/blocks/agent-provider-settings/index.ts +2 -0
  7. package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
  8. package/src/blocks/agent-task-workspace/index.ts +2 -0
  9. package/src/blocks/analysis-context/analysis-context.tsx +6 -0
  10. package/src/blocks/analysis-context/index.ts +1 -0
  11. package/src/blocks/app-header/app-header.tsx +159 -0
  12. package/src/blocks/app-header/index.ts +2 -0
  13. package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
  14. package/src/blocks/archived-chat-workspace/index.ts +2 -0
  15. package/src/blocks/auth/README.md +32 -0
  16. package/src/blocks/auth/admin-login-page.tsx +15 -0
  17. package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
  18. package/src/blocks/auth/auth-page-layout.tsx +90 -0
  19. package/src/blocks/auth/client-login-page.tsx +20 -0
  20. package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
  21. package/src/blocks/auth/identity-management-desk.tsx +530 -0
  22. package/src/blocks/auth/index.ts +12 -0
  23. package/src/blocks/auth/login-page.tsx +206 -0
  24. package/src/blocks/auth/password-forgot-page.tsx +60 -0
  25. package/src/blocks/auth/portal-pages.tsx +32 -0
  26. package/src/blocks/auth/privileged-desk.tsx +55 -0
  27. package/src/blocks/auth/register-page.tsx +117 -0
  28. package/src/blocks/auth/session-boundary.tsx +296 -0
  29. package/src/blocks/auth/super-admin-login-page.tsx +15 -0
  30. package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
  31. package/src/blocks/blog/blog-card.tsx +156 -0
  32. package/src/blocks/blog/blog-reader.tsx +175 -0
  33. package/src/blocks/blog/blog-types.ts +47 -0
  34. package/src/blocks/blog/index.ts +3 -0
  35. package/src/blocks/chat-composer/chat-composer.tsx +140 -0
  36. package/src/blocks/chat-composer/index.ts +1 -0
  37. package/src/blocks/chat-history/chat-history.tsx +59 -0
  38. package/src/blocks/chat-history/index.ts +1 -0
  39. package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
  40. package/src/blocks/chat-response-progress/index.ts +2 -0
  41. package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
  42. package/src/blocks/chat-runtime-controls/index.ts +2 -0
  43. package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
  44. package/src/blocks/chat-runtime-trace/index.ts +2 -0
  45. package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
  46. package/src/blocks/codex-connection-settings/index.ts +1 -0
  47. package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
  48. package/src/blocks/dropzone/dropzone-types.ts +32 -0
  49. package/src/blocks/dropzone/dropzone.tsx +216 -0
  50. package/src/blocks/dropzone/index.ts +3 -0
  51. package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
  52. package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
  53. package/src/blocks/ecommerce/cart/index.ts +2 -0
  54. package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
  55. package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
  56. package/src/blocks/ecommerce/categories/index.ts +2 -0
  57. package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
  58. package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
  59. package/src/blocks/ecommerce/checkout/index.ts +2 -0
  60. package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
  61. package/src/blocks/ecommerce/comparison/index.ts +2 -0
  62. package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
  63. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
  64. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
  65. package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
  66. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
  67. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
  68. package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
  69. package/src/blocks/ecommerce/index.ts +12 -0
  70. package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
  71. package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
  72. package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
  73. package/src/blocks/ecommerce/price-history/index.ts +2 -0
  74. package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
  75. package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
  76. package/src/blocks/ecommerce/reviews/index.ts +2 -0
  77. package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
  78. package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
  79. package/src/blocks/ecommerce/wishlist/index.ts +2 -0
  80. package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
  81. package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
  82. package/src/blocks/execution-status/README.md +25 -0
  83. package/src/blocks/execution-status/execution-status.tsx +135 -0
  84. package/src/blocks/execution-status/index.ts +2 -0
  85. package/src/blocks/file-tree/file-icons.tsx +84 -0
  86. package/src/blocks/file-tree/file-tree-node.tsx +158 -0
  87. package/src/blocks/file-tree/file-tree-types.ts +33 -0
  88. package/src/blocks/file-tree/file-tree.tsx +163 -0
  89. package/src/blocks/file-tree/index.ts +5 -0
  90. package/src/blocks/filter-builder/filter-builder.tsx +169 -0
  91. package/src/blocks/filter-builder/filter-row.tsx +164 -0
  92. package/src/blocks/filter-builder/filter-types.ts +61 -0
  93. package/src/blocks/filter-builder/index.ts +12 -0
  94. package/src/blocks/footer/footer-types.ts +35 -0
  95. package/src/blocks/footer/index.ts +2 -0
  96. package/src/blocks/footer/site-footer.tsx +212 -0
  97. package/src/blocks/form/form-block.tsx +113 -0
  98. package/src/blocks/form/form-lookup-field.tsx +60 -0
  99. package/src/blocks/form/index.ts +4 -0
  100. package/src/blocks/handover-stack/handover-stack.tsx +41 -0
  101. package/src/blocks/handover-stack/index.ts +1 -0
  102. package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
  103. package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
  104. package/src/blocks/idea-handover/index.ts +4 -0
  105. package/src/blocks/kanban/index.ts +11 -0
  106. package/src/blocks/kanban/kanban-board.tsx +146 -0
  107. package/src/blocks/kanban/kanban-card.tsx +149 -0
  108. package/src/blocks/kanban/kanban-column.tsx +102 -0
  109. package/src/blocks/kanban/kanban-types.ts +38 -0
  110. package/src/blocks/loader/global-loader.tsx +90 -0
  111. package/src/blocks/loader/index.ts +1 -0
  112. package/src/blocks/mascot/README.md +21 -0
  113. package/src/blocks/mascot/index.ts +2 -0
  114. package/src/blocks/mascot/mascot.tsx +202 -0
  115. package/src/blocks/master-list/README.md +21 -0
  116. package/src/blocks/master-list/index.ts +12 -0
  117. package/src/blocks/master-list/master-form.tsx +157 -0
  118. package/src/blocks/master-list/master-list-desk.tsx +370 -0
  119. package/src/blocks/master-list/master-list.tsx +189 -0
  120. package/src/blocks/master-list/types.ts +18 -0
  121. package/src/blocks/mermaid-preview/index.ts +1 -0
  122. package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
  123. package/src/blocks/notifications/README.md +7 -0
  124. package/src/blocks/notifications/index.ts +1 -0
  125. package/src/blocks/notifications/notification-center-page.tsx +73 -0
  126. package/src/blocks/pricing/index.ts +7 -0
  127. package/src/blocks/pricing/pricing-table.tsx +173 -0
  128. package/src/blocks/pricing/pricing-types.ts +34 -0
  129. package/src/blocks/product-card/index.ts +2 -0
  130. package/src/blocks/product-card/product-card-types.ts +35 -0
  131. package/src/blocks/product-card/product-card.tsx +183 -0
  132. package/src/blocks/table/data-table-block.tsx +313 -0
  133. package/src/blocks/table/data-table-filter-menu.tsx +48 -0
  134. package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
  135. package/src/blocks/table/data-table-menu-content.tsx +32 -0
  136. package/src/blocks/table/data-table-pagination.tsx +149 -0
  137. package/src/blocks/table/data-table-row-actions.tsx +53 -0
  138. package/src/blocks/table/data-table-status.tsx +42 -0
  139. package/src/blocks/table/data-table-toolbar.tsx +124 -0
  140. package/src/blocks/table/data-table-totals.tsx +31 -0
  141. package/src/blocks/table/index.ts +13 -0
  142. package/src/blocks/workspace/index.ts +19 -0
  143. package/src/blocks/workspace/workspace-action-card.tsx +73 -0
  144. package/src/blocks/workspace/workspace-application-header.tsx +67 -0
  145. package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
  146. package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
  147. package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
  148. package/src/blocks/workspace/workspace-page-header.tsx +43 -0
  149. package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
  150. package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
  151. package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
  152. package/src/blocks/workspace/workspace-section-card.tsx +42 -0
  153. package/src/components/accordion.tsx +65 -0
  154. package/src/components/alert-dialog.tsx +160 -0
  155. package/src/components/alert.tsx +68 -0
  156. package/src/components/aspect-ratio.tsx +22 -0
  157. package/src/components/attachment.tsx +195 -0
  158. package/src/components/avatar.tsx +90 -0
  159. package/src/components/badge.tsx +55 -0
  160. package/src/components/breadcrumb.tsx +100 -0
  161. package/src/components/bubble.tsx +123 -0
  162. package/src/components/button-group.tsx +79 -0
  163. package/src/components/button.tsx +71 -0
  164. package/src/components/calendar.tsx +185 -0
  165. package/src/components/card.tsx +100 -0
  166. package/src/components/carousel.tsx +229 -0
  167. package/src/components/chart.tsx +338 -0
  168. package/src/components/checkbox.tsx +27 -0
  169. package/src/components/collapsible.tsx +15 -0
  170. package/src/components/combobox.tsx +269 -0
  171. package/src/components/command.tsx +181 -0
  172. package/src/components/compact-model-switcher.tsx +128 -0
  173. package/src/components/context-menu.tsx +244 -0
  174. package/src/components/dialog.tsx +138 -0
  175. package/src/components/direction.tsx +1 -0
  176. package/src/components/drawer.tsx +208 -0
  177. package/src/components/dropdown-menu.tsx +257 -0
  178. package/src/components/empty.tsx +93 -0
  179. package/src/components/field.tsx +232 -0
  180. package/src/components/hover-card.tsx +45 -0
  181. package/src/components/input-group.tsx +146 -0
  182. package/src/components/input-otp.tsx +83 -0
  183. package/src/components/input.tsx +19 -0
  184. package/src/components/item.tsx +188 -0
  185. package/src/components/kbd.tsx +26 -0
  186. package/src/components/label.tsx +17 -0
  187. package/src/components/markdown-content.tsx +113 -0
  188. package/src/components/marker.tsx +67 -0
  189. package/src/components/menubar.tsx +267 -0
  190. package/src/components/message-scroller.tsx +91 -0
  191. package/src/components/message.tsx +84 -0
  192. package/src/components/native-select.tsx +54 -0
  193. package/src/components/navigation-menu.tsx +160 -0
  194. package/src/components/pagination.tsx +118 -0
  195. package/src/components/popover.tsx +74 -0
  196. package/src/components/progress.tsx +82 -0
  197. package/src/components/questionnaire.tsx +150 -0
  198. package/src/components/radio-group.tsx +35 -0
  199. package/src/components/resizable.tsx +41 -0
  200. package/src/components/rich-text-editor.tsx +604 -0
  201. package/src/components/scroll-area.tsx +47 -0
  202. package/src/components/select.tsx +189 -0
  203. package/src/components/separator.tsx +18 -0
  204. package/src/components/sheet.tsx +125 -0
  205. package/src/components/sidebar.tsx +693 -0
  206. package/src/components/skeleton.tsx +13 -0
  207. package/src/components/slider.tsx +51 -0
  208. package/src/components/sonner.tsx +45 -0
  209. package/src/components/sparkline.tsx +231 -0
  210. package/src/components/spinner.tsx +20 -0
  211. package/src/components/status-badge.tsx +114 -0
  212. package/src/components/switch.tsx +31 -0
  213. package/src/components/table.tsx +91 -0
  214. package/src/components/tabs.tsx +72 -0
  215. package/src/components/textarea.tsx +17 -0
  216. package/src/components/toast.tsx +222 -0
  217. package/src/components/toggle-group.tsx +85 -0
  218. package/src/components/toggle.tsx +42 -0
  219. package/src/components/tooltip.tsx +53 -0
  220. package/src/design-system/README.md +55 -0
  221. package/src/design-system/action-intents.ts +21 -0
  222. package/src/design-system/agent-manifest.ts +83 -0
  223. package/src/design-system/block-registry.ts +272 -0
  224. package/src/design-system/component-registry.ts +142 -0
  225. package/src/design-system/contracts.ts +69 -0
  226. package/src/design-system/defaults.ts +3 -0
  227. package/src/design-system/index.ts +29 -0
  228. package/src/design-system/mcp-server.ts +156 -0
  229. package/src/design-system/page-registry.ts +74 -0
  230. package/src/design-system/selection.ts +67 -0
  231. package/src/design-system/template-registry.ts +33 -0
  232. package/src/features/interface-topology/index.ts +14 -0
  233. package/src/features/interface-topology/interface-topology.types.ts +40 -0
  234. package/src/features/interface-topology/topology-control.tsx +18 -0
  235. package/src/features/interface-topology/topology-inspector.tsx +228 -0
  236. package/src/features/interface-topology/topology-marker.tsx +47 -0
  237. package/src/features/interface-topology/topology-region.tsx +44 -0
  238. package/src/features/interface-topology/use-interface-topology.ts +178 -0
  239. package/src/hooks/use-mobile.ts +19 -0
  240. package/src/hooks/use-scroll-follow.ts +22 -0
  241. package/src/index.ts +11 -0
  242. package/src/layouts/README.md +65 -0
  243. package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
  244. package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
  245. package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
  246. package/src/layouts/agent-workspace/index.ts +7 -0
  247. package/src/layouts/blog-header/blog-header-types.ts +25 -0
  248. package/src/layouts/blog-header/blog-header.tsx +172 -0
  249. package/src/layouts/blog-header/index.ts +2 -0
  250. package/src/layouts/documentation-workspace/README.md +8 -0
  251. package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
  252. package/src/layouts/documentation-workspace/index.ts +1 -0
  253. package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
  254. package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
  255. package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
  256. package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
  257. package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
  258. package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
  259. package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
  260. package/src/layouts/ecommerce-header/index.ts +2 -0
  261. package/src/layouts/main-workspace/index.ts +28 -0
  262. package/src/layouts/mdi/index.ts +1 -0
  263. package/src/layouts/mdi/mdi.tsx +20 -0
  264. package/src/layouts/mdi-main/index.ts +27 -0
  265. package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
  266. package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
  267. package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
  268. package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
  269. package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
  270. package/src/layouts/mdi-main/mdi-main.tsx +241 -0
  271. package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
  272. package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
  273. package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
  274. package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
  275. package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
  276. package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
  277. package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
  278. package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
  279. package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
  280. package/src/layouts/mdi-main/mdi-types.ts +131 -0
  281. package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
  282. package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
  283. package/src/layouts/site-header/announcement-bar.tsx +55 -0
  284. package/src/layouts/site-header/category-strip.tsx +41 -0
  285. package/src/layouts/site-header/index.ts +16 -0
  286. package/src/layouts/site-header/main-navbar.tsx +179 -0
  287. package/src/layouts/site-header/mobile-drawer.tsx +140 -0
  288. package/src/layouts/site-header/site-header-types.ts +63 -0
  289. package/src/layouts/site-header/site-header.tsx +58 -0
  290. package/src/lib/random-id.ts +19 -0
  291. package/src/lib/utils.ts +6 -0
  292. package/src/providers/pdf-provider.ts +137 -0
  293. package/src/styles/globals.css +374 -0
  294. package/src/templates/README.md +12 -0
  295. package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
  296. package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
  297. package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
  298. package/src/templates/dashboard-01/components/data-table.tsx +577 -0
  299. package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
  300. package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
  301. package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
  302. package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
  303. package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
  304. package/src/templates/dashboard-01/components/site-header.tsx +14 -0
  305. package/src/templates/dashboard-01/data.json +614 -0
  306. package/src/templates/dashboard-01/index.ts +7 -0
  307. package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
  308. package/src/templates/documentation-sidebar/index.ts +2 -0
  309. package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
  310. package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
  311. package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
  312. package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
  313. package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
  314. package/src/templates/sidebar-07/index.ts +1 -0
  315. package/src/templates/ui-page/index.ts +7 -0
  316. package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
  317. package/src/templates/ui-page/ui-template-code.tsx +48 -0
  318. package/src/templates/ui-page/ui-template-header.tsx +87 -0
  319. package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
  320. package/src/templates/ui-page/ui-template-page.tsx +97 -0
  321. package/src/theme/index.ts +12 -0
  322. package/src/theme/theme-config.ts +30 -0
  323. package/src/theme/theme-provider.tsx +142 -0
  324. package/src/theme/theme-selector.tsx +75 -0
  325. package/src/tokens/index.ts +15 -0
  326. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,217 @@
1
+ import { useState } from 'react'
2
+ import { ArrowDownIcon, BellIcon, CheckIcon, TrendingDownIcon } from 'lucide-react'
3
+ import { cn } from '../../../lib/utils'
4
+ import { Button } from '../../../components/button'
5
+ import { Badge } from '../../../components/badge'
6
+ import type { PriceHistoryProps } from './price-history-types'
7
+
8
+ export function PriceHistoryChart({
9
+ averagePrice: propAvg,
10
+ className,
11
+ currencySymbol = '$',
12
+ currentPrice,
13
+ history,
14
+ lowestPrice: propLow,
15
+ onSetPriceAlert,
16
+ peakPrice: propHigh,
17
+ productTitle,
18
+ }: PriceHistoryProps) {
19
+ const [alertSet, setAlertSet] = useState(false)
20
+
21
+ const prices = history.map((h) => h.price)
22
+ const lowest = propLow ?? Math.min(...prices, currentPrice)
23
+ const peak = propHigh ?? Math.max(...prices, currentPrice)
24
+ const avg = propAvg ?? Math.round(prices.reduce((a, b) => a + b, 0) / (prices.length || 1))
25
+
26
+ const isAtLowest = currentPrice <= lowest
27
+ const savingsFromPeak = Math.max(0, peak - currentPrice)
28
+
29
+ // SVG dimensions
30
+ const width = 500
31
+ const height = 160
32
+ const padding = 24
33
+ const range = Math.max(1, peak - lowest)
34
+
35
+ // Points
36
+ const points = history.map((pt, idx) => {
37
+ const x = padding + (idx / Math.max(1, history.length - 1)) * (width - 2 * padding)
38
+ const y = height - padding - ((pt.price - lowest) / range) * (height - 2 * padding)
39
+ return { ...pt, x, y }
40
+ })
41
+
42
+ const pathD = points.reduce(
43
+ (acc, pt, idx) => `${acc} ${idx === 0 ? 'M' : 'L'} ${pt.x} ${pt.y}`,
44
+ '',
45
+ )
46
+ const areaD =
47
+ points.length > 0
48
+ ? `${pathD} L ${points[points.length - 1]!.x} ${height - padding} L ${points[0]!.x} ${height - padding} Z`
49
+ : ''
50
+
51
+ const handleAlert = () => {
52
+ setAlertSet(true)
53
+ onSetPriceAlert?.()
54
+ }
55
+
56
+ return (
57
+ <div
58
+ className={cn(
59
+ 'space-y-4 rounded-2xl border border-border/80 bg-card p-5 shadow-xs',
60
+ className,
61
+ )}
62
+ >
63
+ {/* Header */}
64
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 border-b border-border/60 pb-4">
65
+ <div>
66
+ <div className="flex items-center gap-2">
67
+ <TrendingDownIcon className="size-4 text-emerald-600" />
68
+ <h4 className="text-sm font-bold text-foreground">Price History Tracker</h4>
69
+ {isAtLowest && (
70
+ <Badge
71
+ variant="default"
72
+ className="text-[10px] font-bold bg-emerald-600 hover:bg-emerald-700"
73
+ >
74
+ All-Time Low!
75
+ </Badge>
76
+ )}
77
+ </div>
78
+ {productTitle && (
79
+ <p className="text-xs text-muted-foreground mt-0.5 truncate">{productTitle}</p>
80
+ )}
81
+ </div>
82
+
83
+ <Button
84
+ size="sm"
85
+ variant={alertSet ? 'secondary' : 'outline'}
86
+ className="h-8 gap-1.5 text-xs self-start sm:self-auto"
87
+ onClick={handleAlert}
88
+ >
89
+ {alertSet ? (
90
+ <>
91
+ <CheckIcon className="size-3.5 text-emerald-600" />
92
+ <span>Alert Enabled</span>
93
+ </>
94
+ ) : (
95
+ <>
96
+ <BellIcon className="size-3.5 text-primary" />
97
+ <span>Notify Price Drops</span>
98
+ </>
99
+ )}
100
+ </Button>
101
+ </div>
102
+
103
+ {/* Stats row */}
104
+ <div className="grid grid-cols-3 gap-3 rounded-xl border border-border/60 bg-muted/20 p-3 text-center text-xs">
105
+ <div>
106
+ <span className="text-muted-foreground text-[11px]">Current</span>
107
+ <div className="font-bold text-foreground text-sm mt-0.5">
108
+ {currencySymbol}
109
+ {currentPrice}
110
+ </div>
111
+ </div>
112
+ <div>
113
+ <span className="text-muted-foreground text-[11px]">Lowest Recorded</span>
114
+ <div className="font-bold text-emerald-600 text-sm mt-0.5">
115
+ {currencySymbol}
116
+ {lowest}
117
+ </div>
118
+ </div>
119
+ <div>
120
+ <span className="text-muted-foreground text-[11px]">Average</span>
121
+ <div className="font-bold text-foreground text-sm mt-0.5">
122
+ {currencySymbol}
123
+ {avg}
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ {/* SVG Price Chart */}
129
+ <div className="relative w-full overflow-hidden rounded-xl border border-border/40 bg-muted/10 p-2">
130
+ <svg viewBox={`0 0 ${width} ${height}`} className="w-full h-40 overflow-visible">
131
+ <defs>
132
+ <linearGradient id="priceGrad" x1="0" y1="0" x2="0" y2="1">
133
+ <stop offset="0%" stopColor="var(--primary, #3b82f6)" stopOpacity="0.25" />
134
+ <stop offset="100%" stopColor="var(--primary, #3b82f6)" stopOpacity="0.0" />
135
+ </linearGradient>
136
+ </defs>
137
+
138
+ {/* Grid lines */}
139
+ <line
140
+ x1={padding}
141
+ y1={padding}
142
+ x2={width - padding}
143
+ y2={padding}
144
+ stroke="currentColor"
145
+ strokeOpacity="0.1"
146
+ strokeDasharray="3 3"
147
+ />
148
+ <line
149
+ x1={padding}
150
+ y1={height / 2}
151
+ x2={width - padding}
152
+ y2={height / 2}
153
+ stroke="currentColor"
154
+ strokeOpacity="0.1"
155
+ strokeDasharray="3 3"
156
+ />
157
+ <line
158
+ x1={padding}
159
+ y1={height - padding}
160
+ x2={width - padding}
161
+ y2={height - padding}
162
+ stroke="currentColor"
163
+ strokeOpacity="0.1"
164
+ />
165
+
166
+ {/* Gradient area */}
167
+ {areaD && <path d={areaD} fill="url(#priceGrad)" />}
168
+
169
+ {/* Path line */}
170
+ {pathD && (
171
+ <path
172
+ d={pathD}
173
+ fill="none"
174
+ stroke="var(--primary, #3b82f6)"
175
+ strokeWidth="2.5"
176
+ strokeLinecap="round"
177
+ strokeLinejoin="round"
178
+ />
179
+ )}
180
+
181
+ {/* Data Points */}
182
+ {points.map((pt, i) => (
183
+ <g key={i}>
184
+ <circle
185
+ cx={pt.x}
186
+ cy={pt.y}
187
+ r="3.5"
188
+ className="fill-background stroke-primary stroke-2 hover:r-5 transition-all"
189
+ />
190
+ </g>
191
+ ))}
192
+ </svg>
193
+
194
+ {/* Date labels */}
195
+ <div className="flex justify-between px-4 text-[10px] text-muted-foreground font-mono">
196
+ <span>{history[0]?.date}</span>
197
+ <span>{history[history.length - 1]?.date}</span>
198
+ </div>
199
+ </div>
200
+
201
+ {savingsFromPeak > 0 && (
202
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
203
+ <ArrowDownIcon className="size-3.5 text-emerald-600 font-bold" />
204
+ <span>
205
+ Currently{' '}
206
+ <strong className="text-emerald-600">
207
+ {currencySymbol}
208
+ {savingsFromPeak} cheaper
209
+ </strong>{' '}
210
+ than its peak historical price of {currencySymbol}
211
+ {peak}.
212
+ </span>
213
+ </div>
214
+ )}
215
+ </div>
216
+ )
217
+ }
@@ -0,0 +1,16 @@
1
+ export type PriceDataPoint = {
2
+ readonly date: string
3
+ readonly price: number
4
+ }
5
+
6
+ export type PriceHistoryProps = {
7
+ readonly averagePrice?: number
8
+ readonly className?: string
9
+ readonly currencySymbol?: string
10
+ readonly currentPrice: number
11
+ readonly history: readonly PriceDataPoint[]
12
+ readonly lowestPrice?: number
13
+ readonly onSetPriceAlert?: () => void
14
+ readonly peakPrice?: number
15
+ readonly productTitle?: string
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './reviews-section'
2
+ export * from './reviews-types'
@@ -0,0 +1,309 @@
1
+ import { useState } from 'react'
2
+ import {
3
+ CheckCircle2Icon,
4
+ MessageSquareIcon,
5
+ PenToolIcon,
6
+ StarIcon,
7
+ ThumbsUpIcon,
8
+ } from 'lucide-react'
9
+ import { cn } from '../../../lib/utils'
10
+ import { Button } from '../../../components/button'
11
+ import { Badge } from '../../../components/badge'
12
+ import type { ReviewsSectionProps } from './reviews-types'
13
+
14
+ export function ReviewsSection({
15
+ averageRating,
16
+ className,
17
+ distribution,
18
+ onHelpfulVote,
19
+ onSubmitReview,
20
+ reviews,
21
+ totalReviews,
22
+ }: ReviewsSectionProps) {
23
+ const [showWriteModal, setShowWriteModal] = useState(false)
24
+ const [selectedStarFilter, setSelectedStarFilter] = useState<number | null>(null)
25
+ const [newRating, setNewRating] = useState(5)
26
+ const [newTitle, setNewTitle] = useState('')
27
+ const [newComment, setNewComment] = useState('')
28
+ const [votedSet, setVotedSet] = useState<Set<string>>(new Set())
29
+
30
+ const filteredReviews = selectedStarFilter
31
+ ? reviews.filter((r) => r.rating === selectedStarFilter)
32
+ : reviews
33
+
34
+ const handleVote = (id: string) => {
35
+ if (votedSet.has(id)) return
36
+ setVotedSet(new Set(votedSet).add(id))
37
+ onHelpfulVote?.(id)
38
+ }
39
+
40
+ const handleSubmit = (e: React.FormEvent) => {
41
+ e.preventDefault()
42
+ if (!newComment.trim()) return
43
+ onSubmitReview?.({
44
+ comment: newComment,
45
+ rating: newRating,
46
+ title: newTitle,
47
+ })
48
+ setShowWriteModal(false)
49
+ setNewTitle('')
50
+ setNewComment('')
51
+ }
52
+
53
+ return (
54
+ <div
55
+ className={cn(
56
+ 'space-y-8 rounded-2xl border border-border/80 bg-card p-6 shadow-xs',
57
+ className,
58
+ )}
59
+ >
60
+ {/* 1. Rating Summary Header */}
61
+ <div className="grid grid-cols-1 md:grid-cols-12 gap-8 border-b border-border/60 pb-8">
62
+ {/* Big Score */}
63
+ <div className="md:col-span-4 flex flex-col items-center justify-center p-6 rounded-2xl bg-muted/20 border border-border/60 text-center">
64
+ <span className="text-4xl font-black tracking-tight text-foreground">
65
+ {averageRating.toFixed(1)}
66
+ </span>
67
+ <div className="flex items-center gap-1 my-2">
68
+ {[1, 2, 3, 4, 5].map((s) => (
69
+ <StarIcon
70
+ key={s}
71
+ className={cn(
72
+ 'size-4',
73
+ s <= Math.round(averageRating)
74
+ ? 'fill-amber-400 text-amber-400'
75
+ : 'fill-muted text-muted-foreground/30',
76
+ )}
77
+ />
78
+ ))}
79
+ </div>
80
+ <span className="text-xs text-muted-foreground">
81
+ Based on {totalReviews.toLocaleString()} verified ratings
82
+ </span>
83
+
84
+ <Button size="sm" className="gap-2 mt-4 text-xs" onClick={() => setShowWriteModal(true)}>
85
+ <PenToolIcon className="size-3.5" />
86
+ <span>Write a Review</span>
87
+ </Button>
88
+ </div>
89
+
90
+ {/* 5-Star Distribution Bars */}
91
+ <div className="md:col-span-8 flex flex-col justify-center space-y-2 text-xs">
92
+ {[5, 4, 3, 2, 1].map((star) => {
93
+ const count = distribution[star] ?? 0
94
+ const pct = totalReviews > 0 ? Math.round((count / totalReviews) * 100) : 0
95
+ const isSelected = selectedStarFilter === star
96
+
97
+ return (
98
+ <button
99
+ key={star}
100
+ type="button"
101
+ onClick={() => setSelectedStarFilter(isSelected ? null : star)}
102
+ className={cn(
103
+ 'flex items-center gap-3 rounded-lg px-2 py-1 transition-all text-left group',
104
+ isSelected
105
+ ? 'bg-primary/10 font-bold text-primary'
106
+ : 'hover:bg-muted/40 text-muted-foreground',
107
+ )}
108
+ >
109
+ <div className="flex items-center gap-1 w-12 shrink-0">
110
+ <span className="font-semibold text-foreground">{star}</span>
111
+ <StarIcon className="size-3 fill-amber-400 text-amber-400" />
112
+ </div>
113
+
114
+ <div className="h-2 flex-1 rounded-full bg-border overflow-hidden">
115
+ <div
116
+ className={cn(
117
+ 'h-full transition-all duration-300',
118
+ isSelected ? 'bg-primary' : 'bg-amber-400',
119
+ )}
120
+ style={{ width: `${pct}%` }}
121
+ />
122
+ </div>
123
+
124
+ <span className="w-12 text-right text-[11px] font-mono text-muted-foreground">
125
+ {pct}%
126
+ </span>
127
+ </button>
128
+ )
129
+ })}
130
+ </div>
131
+ </div>
132
+
133
+ {/* 2. Write Review Modal / Inline Form */}
134
+ {showWriteModal && (
135
+ <form
136
+ onSubmit={handleSubmit}
137
+ className="rounded-xl border border-border/80 bg-muted/20 p-5 space-y-4 animate-in fade-in-0 duration-200"
138
+ >
139
+ <div className="flex items-center justify-between border-b border-border/60 pb-3">
140
+ <h4 className="text-sm font-bold text-foreground">Write Customer Review</h4>
141
+ <button
142
+ type="button"
143
+ onClick={() => setShowWriteModal(false)}
144
+ className="text-xs text-muted-foreground hover:text-foreground"
145
+ >
146
+ Cancel
147
+ </button>
148
+ </div>
149
+
150
+ <div className="space-y-1">
151
+ <label className="text-xs font-semibold text-foreground">Overall Rating</label>
152
+ <div className="flex items-center gap-1.5">
153
+ {[1, 2, 3, 4, 5].map((s) => (
154
+ <button
155
+ key={s}
156
+ type="button"
157
+ onClick={() => setNewRating(s)}
158
+ className="p-1 hover:scale-110 transition-transform"
159
+ >
160
+ <StarIcon
161
+ className={cn(
162
+ 'size-5',
163
+ s <= newRating ? 'fill-amber-400 text-amber-400' : 'text-border',
164
+ )}
165
+ />
166
+ </button>
167
+ ))}
168
+ </div>
169
+ </div>
170
+
171
+ <div className="space-y-1">
172
+ <label className="text-xs font-semibold text-foreground">Review Title (optional)</label>
173
+ <input
174
+ type="text"
175
+ placeholder="e.g. Exceeded expectations! Great battery life."
176
+ value={newTitle}
177
+ onChange={(e) => setNewTitle(e.target.value)}
178
+ className="w-full rounded-lg border border-input bg-background px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary"
179
+ />
180
+ </div>
181
+
182
+ <div className="space-y-1">
183
+ <label className="text-xs font-semibold text-foreground">Your Feedback</label>
184
+ <textarea
185
+ rows={3}
186
+ required
187
+ placeholder="What did you like or dislike about this product?"
188
+ value={newComment}
189
+ onChange={(e) => setNewComment(e.target.value)}
190
+ className="w-full rounded-lg border border-input bg-background px-3 py-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary"
191
+ />
192
+ </div>
193
+
194
+ <Button type="submit" size="sm" className="text-xs">
195
+ Submit Review
196
+ </Button>
197
+ </form>
198
+ )}
199
+
200
+ {/* 3. Reviews List */}
201
+ <div className="space-y-4">
202
+ <div className="flex items-center justify-between">
203
+ <div className="flex items-center gap-2">
204
+ <MessageSquareIcon className="size-4 text-primary" />
205
+ <h4 className="text-sm font-bold text-foreground">
206
+ Customer Feedback ({filteredReviews.length})
207
+ </h4>
208
+ </div>
209
+ {selectedStarFilter && (
210
+ <Badge
211
+ variant="outline"
212
+ className="cursor-pointer gap-1 text-xs"
213
+ onClick={() => setSelectedStarFilter(null)}
214
+ >
215
+ <span>Showing {selectedStarFilter}-star only</span>
216
+ <span>✕</span>
217
+ </Badge>
218
+ )}
219
+ </div>
220
+
221
+ <div className="space-y-4 divide-y divide-border/60">
222
+ {filteredReviews.map((rev) => {
223
+ const hasVoted = votedSet.has(rev.id)
224
+
225
+ return (
226
+ <div key={rev.id} className="pt-4 first:pt-0 space-y-2">
227
+ <div className="flex items-start justify-between gap-4">
228
+ <div className="flex items-center gap-2.5">
229
+ {rev.authorAvatar ? (
230
+ <img
231
+ src={rev.authorAvatar}
232
+ alt={rev.author}
233
+ className="size-8 rounded-full object-cover"
234
+ />
235
+ ) : (
236
+ <div className="flex size-8 items-center justify-center rounded-full bg-primary/10 text-primary font-bold text-xs">
237
+ {rev.author.charAt(0)}
238
+ </div>
239
+ )}
240
+ <div>
241
+ <div className="flex items-center gap-1.5">
242
+ <span className="text-xs font-bold text-foreground">{rev.author}</span>
243
+ {rev.isVerifiedBuyer && (
244
+ <span className="inline-flex items-center gap-0.5 text-[10px] font-semibold text-emerald-600 dark:text-emerald-400">
245
+ <CheckCircle2Icon className="size-3" />
246
+ Verified Buyer
247
+ </span>
248
+ )}
249
+ </div>
250
+ <span className="text-[10px] text-muted-foreground">{rev.date}</span>
251
+ </div>
252
+ </div>
253
+
254
+ <div className="flex items-center gap-0.5">
255
+ {[1, 2, 3, 4, 5].map((s) => (
256
+ <StarIcon
257
+ key={s}
258
+ className={cn(
259
+ 'size-3.5',
260
+ s <= rev.rating
261
+ ? 'fill-amber-400 text-amber-400'
262
+ : 'text-muted-foreground/30',
263
+ )}
264
+ />
265
+ ))}
266
+ </div>
267
+ </div>
268
+
269
+ {rev.title && <h5 className="text-xs font-bold text-foreground">{rev.title}</h5>}
270
+ <p className="text-xs text-muted-foreground leading-relaxed">{rev.comment}</p>
271
+
272
+ {rev.photos && rev.photos.length > 0 && (
273
+ <div className="flex gap-2 pt-1">
274
+ {rev.photos.map((ph, idx) => (
275
+ <img
276
+ key={idx}
277
+ src={ph}
278
+ alt="Customer photo"
279
+ className="size-14 rounded-lg object-cover border border-border"
280
+ />
281
+ ))}
282
+ </div>
283
+ )}
284
+
285
+ {/* Helpful Button */}
286
+ <div className="flex items-center gap-2 pt-1">
287
+ <button
288
+ type="button"
289
+ disabled={hasVoted}
290
+ onClick={() => handleVote(rev.id)}
291
+ className={cn(
292
+ 'flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-[11px] font-medium transition-colors',
293
+ hasVoted
294
+ ? 'border-emerald-600/30 bg-emerald-500/10 text-emerald-600'
295
+ : 'border-border/80 text-muted-foreground hover:bg-muted hover:text-foreground',
296
+ )}
297
+ >
298
+ <ThumbsUpIcon className="size-3" />
299
+ <span>Helpful ({(rev.helpfulCount ?? 0) + (hasVoted ? 1 : 0)})</span>
300
+ </button>
301
+ </div>
302
+ </div>
303
+ )
304
+ })}
305
+ </div>
306
+ </div>
307
+ </div>
308
+ )
309
+ }
@@ -0,0 +1,26 @@
1
+ export type CustomerReview = {
2
+ readonly author: string
3
+ readonly authorAvatar?: string
4
+ readonly comment: string
5
+ readonly date: string
6
+ readonly helpfulCount?: number
7
+ readonly id: string
8
+ readonly isVerifiedBuyer?: boolean
9
+ readonly photos?: readonly string[]
10
+ readonly rating: number
11
+ readonly title?: string
12
+ }
13
+
14
+ export type RatingDistribution = {
15
+ readonly [star: number]: number
16
+ }
17
+
18
+ export type ReviewsSectionProps = {
19
+ readonly averageRating: number
20
+ readonly className?: string
21
+ readonly distribution: RatingDistribution
22
+ readonly onHelpfulVote?: (reviewId: string) => void
23
+ readonly onSubmitReview?: (review: { comment: string; rating: number; title: string }) => void
24
+ readonly reviews: readonly CustomerReview[]
25
+ readonly totalReviews: number
26
+ }
@@ -0,0 +1,2 @@
1
+ export * from './wishlist-grid'
2
+ export * from './wishlist-types'