@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.
- package/README.md +85 -2
- package/package.json +105 -4
- package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
- package/src/blocks/agent-chat-workspace/index.ts +2 -0
- package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
- package/src/blocks/agent-provider-settings/index.ts +2 -0
- package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
- package/src/blocks/agent-task-workspace/index.ts +2 -0
- package/src/blocks/analysis-context/analysis-context.tsx +6 -0
- package/src/blocks/analysis-context/index.ts +1 -0
- package/src/blocks/app-header/app-header.tsx +159 -0
- package/src/blocks/app-header/index.ts +2 -0
- package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
- package/src/blocks/archived-chat-workspace/index.ts +2 -0
- package/src/blocks/auth/README.md +32 -0
- package/src/blocks/auth/admin-login-page.tsx +15 -0
- package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/auth-page-layout.tsx +90 -0
- package/src/blocks/auth/client-login-page.tsx +20 -0
- package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/identity-management-desk.tsx +530 -0
- package/src/blocks/auth/index.ts +12 -0
- package/src/blocks/auth/login-page.tsx +206 -0
- package/src/blocks/auth/password-forgot-page.tsx +60 -0
- package/src/blocks/auth/portal-pages.tsx +32 -0
- package/src/blocks/auth/privileged-desk.tsx +55 -0
- package/src/blocks/auth/register-page.tsx +117 -0
- package/src/blocks/auth/session-boundary.tsx +296 -0
- package/src/blocks/auth/super-admin-login-page.tsx +15 -0
- package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
- package/src/blocks/blog/blog-card.tsx +156 -0
- package/src/blocks/blog/blog-reader.tsx +175 -0
- package/src/blocks/blog/blog-types.ts +47 -0
- package/src/blocks/blog/index.ts +3 -0
- package/src/blocks/chat-composer/chat-composer.tsx +140 -0
- package/src/blocks/chat-composer/index.ts +1 -0
- package/src/blocks/chat-history/chat-history.tsx +59 -0
- package/src/blocks/chat-history/index.ts +1 -0
- package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
- package/src/blocks/chat-response-progress/index.ts +2 -0
- package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
- package/src/blocks/chat-runtime-controls/index.ts +2 -0
- package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
- package/src/blocks/chat-runtime-trace/index.ts +2 -0
- package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
- package/src/blocks/codex-connection-settings/index.ts +1 -0
- package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
- package/src/blocks/dropzone/dropzone-types.ts +32 -0
- package/src/blocks/dropzone/dropzone.tsx +216 -0
- package/src/blocks/dropzone/index.ts +3 -0
- package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
- package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
- package/src/blocks/ecommerce/cart/index.ts +2 -0
- package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
- package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
- package/src/blocks/ecommerce/categories/index.ts +2 -0
- package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
- package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
- package/src/blocks/ecommerce/checkout/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
- package/src/blocks/ecommerce/comparison/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
- package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
- package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
- package/src/blocks/ecommerce/index.ts +12 -0
- package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
- package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
- package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
- package/src/blocks/ecommerce/price-history/index.ts +2 -0
- package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
- package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
- package/src/blocks/ecommerce/reviews/index.ts +2 -0
- package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
- package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
- package/src/blocks/ecommerce/wishlist/index.ts +2 -0
- package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
- package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
- package/src/blocks/execution-status/README.md +25 -0
- package/src/blocks/execution-status/execution-status.tsx +135 -0
- package/src/blocks/execution-status/index.ts +2 -0
- package/src/blocks/file-tree/file-icons.tsx +84 -0
- package/src/blocks/file-tree/file-tree-node.tsx +158 -0
- package/src/blocks/file-tree/file-tree-types.ts +33 -0
- package/src/blocks/file-tree/file-tree.tsx +163 -0
- package/src/blocks/file-tree/index.ts +5 -0
- package/src/blocks/filter-builder/filter-builder.tsx +169 -0
- package/src/blocks/filter-builder/filter-row.tsx +164 -0
- package/src/blocks/filter-builder/filter-types.ts +61 -0
- package/src/blocks/filter-builder/index.ts +12 -0
- package/src/blocks/footer/footer-types.ts +35 -0
- package/src/blocks/footer/index.ts +2 -0
- package/src/blocks/footer/site-footer.tsx +212 -0
- package/src/blocks/form/form-block.tsx +113 -0
- package/src/blocks/form/form-lookup-field.tsx +60 -0
- package/src/blocks/form/index.ts +4 -0
- package/src/blocks/handover-stack/handover-stack.tsx +41 -0
- package/src/blocks/handover-stack/index.ts +1 -0
- package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
- package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
- package/src/blocks/idea-handover/index.ts +4 -0
- package/src/blocks/kanban/index.ts +11 -0
- package/src/blocks/kanban/kanban-board.tsx +146 -0
- package/src/blocks/kanban/kanban-card.tsx +149 -0
- package/src/blocks/kanban/kanban-column.tsx +102 -0
- package/src/blocks/kanban/kanban-types.ts +38 -0
- package/src/blocks/loader/global-loader.tsx +90 -0
- package/src/blocks/loader/index.ts +1 -0
- package/src/blocks/mascot/README.md +21 -0
- package/src/blocks/mascot/index.ts +2 -0
- package/src/blocks/mascot/mascot.tsx +202 -0
- package/src/blocks/master-list/README.md +21 -0
- package/src/blocks/master-list/index.ts +12 -0
- package/src/blocks/master-list/master-form.tsx +157 -0
- package/src/blocks/master-list/master-list-desk.tsx +370 -0
- package/src/blocks/master-list/master-list.tsx +189 -0
- package/src/blocks/master-list/types.ts +18 -0
- package/src/blocks/mermaid-preview/index.ts +1 -0
- package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
- package/src/blocks/notifications/README.md +7 -0
- package/src/blocks/notifications/index.ts +1 -0
- package/src/blocks/notifications/notification-center-page.tsx +73 -0
- package/src/blocks/pricing/index.ts +7 -0
- package/src/blocks/pricing/pricing-table.tsx +173 -0
- package/src/blocks/pricing/pricing-types.ts +34 -0
- package/src/blocks/product-card/index.ts +2 -0
- package/src/blocks/product-card/product-card-types.ts +35 -0
- package/src/blocks/product-card/product-card.tsx +183 -0
- package/src/blocks/table/data-table-block.tsx +313 -0
- package/src/blocks/table/data-table-filter-menu.tsx +48 -0
- package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
- package/src/blocks/table/data-table-menu-content.tsx +32 -0
- package/src/blocks/table/data-table-pagination.tsx +149 -0
- package/src/blocks/table/data-table-row-actions.tsx +53 -0
- package/src/blocks/table/data-table-status.tsx +42 -0
- package/src/blocks/table/data-table-toolbar.tsx +124 -0
- package/src/blocks/table/data-table-totals.tsx +31 -0
- package/src/blocks/table/index.ts +13 -0
- package/src/blocks/workspace/index.ts +19 -0
- package/src/blocks/workspace/workspace-action-card.tsx +73 -0
- package/src/blocks/workspace/workspace-application-header.tsx +67 -0
- package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
- package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
- package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
- package/src/blocks/workspace/workspace-page-header.tsx +43 -0
- package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
- package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
- package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
- package/src/blocks/workspace/workspace-section-card.tsx +42 -0
- package/src/components/accordion.tsx +65 -0
- package/src/components/alert-dialog.tsx +160 -0
- package/src/components/alert.tsx +68 -0
- package/src/components/aspect-ratio.tsx +22 -0
- package/src/components/attachment.tsx +195 -0
- package/src/components/avatar.tsx +90 -0
- package/src/components/badge.tsx +55 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/bubble.tsx +123 -0
- package/src/components/button-group.tsx +79 -0
- package/src/components/button.tsx +71 -0
- package/src/components/calendar.tsx +185 -0
- package/src/components/card.tsx +100 -0
- package/src/components/carousel.tsx +229 -0
- package/src/components/chart.tsx +338 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +15 -0
- package/src/components/combobox.tsx +269 -0
- package/src/components/command.tsx +181 -0
- package/src/components/compact-model-switcher.tsx +128 -0
- package/src/components/context-menu.tsx +244 -0
- package/src/components/dialog.tsx +138 -0
- package/src/components/direction.tsx +1 -0
- package/src/components/drawer.tsx +208 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/empty.tsx +93 -0
- package/src/components/field.tsx +232 -0
- package/src/components/hover-card.tsx +45 -0
- package/src/components/input-group.tsx +146 -0
- package/src/components/input-otp.tsx +83 -0
- package/src/components/input.tsx +19 -0
- package/src/components/item.tsx +188 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +17 -0
- package/src/components/markdown-content.tsx +113 -0
- package/src/components/marker.tsx +67 -0
- package/src/components/menubar.tsx +267 -0
- package/src/components/message-scroller.tsx +91 -0
- package/src/components/message.tsx +84 -0
- package/src/components/native-select.tsx +54 -0
- package/src/components/navigation-menu.tsx +160 -0
- package/src/components/pagination.tsx +118 -0
- package/src/components/popover.tsx +74 -0
- package/src/components/progress.tsx +82 -0
- package/src/components/questionnaire.tsx +150 -0
- package/src/components/radio-group.tsx +35 -0
- package/src/components/resizable.tsx +41 -0
- package/src/components/rich-text-editor.tsx +604 -0
- package/src/components/scroll-area.tsx +47 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +18 -0
- package/src/components/sheet.tsx +125 -0
- package/src/components/sidebar.tsx +693 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +51 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/sparkline.tsx +231 -0
- package/src/components/spinner.tsx +20 -0
- package/src/components/status-badge.tsx +114 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +91 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toast.tsx +222 -0
- package/src/components/toggle-group.tsx +85 -0
- package/src/components/toggle.tsx +42 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/design-system/README.md +55 -0
- package/src/design-system/action-intents.ts +21 -0
- package/src/design-system/agent-manifest.ts +83 -0
- package/src/design-system/block-registry.ts +272 -0
- package/src/design-system/component-registry.ts +142 -0
- package/src/design-system/contracts.ts +69 -0
- package/src/design-system/defaults.ts +3 -0
- package/src/design-system/index.ts +29 -0
- package/src/design-system/mcp-server.ts +156 -0
- package/src/design-system/page-registry.ts +74 -0
- package/src/design-system/selection.ts +67 -0
- package/src/design-system/template-registry.ts +33 -0
- package/src/features/interface-topology/index.ts +14 -0
- package/src/features/interface-topology/interface-topology.types.ts +40 -0
- package/src/features/interface-topology/topology-control.tsx +18 -0
- package/src/features/interface-topology/topology-inspector.tsx +228 -0
- package/src/features/interface-topology/topology-marker.tsx +47 -0
- package/src/features/interface-topology/topology-region.tsx +44 -0
- package/src/features/interface-topology/use-interface-topology.ts +178 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scroll-follow.ts +22 -0
- package/src/index.ts +11 -0
- package/src/layouts/README.md +65 -0
- package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
- package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
- package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
- package/src/layouts/agent-workspace/index.ts +7 -0
- package/src/layouts/blog-header/blog-header-types.ts +25 -0
- package/src/layouts/blog-header/blog-header.tsx +172 -0
- package/src/layouts/blog-header/index.ts +2 -0
- package/src/layouts/documentation-workspace/README.md +8 -0
- package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
- package/src/layouts/documentation-workspace/index.ts +1 -0
- package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
- package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
- package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
- package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
- package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
- package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
- package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
- package/src/layouts/ecommerce-header/index.ts +2 -0
- package/src/layouts/main-workspace/index.ts +28 -0
- package/src/layouts/mdi/index.ts +1 -0
- package/src/layouts/mdi/mdi.tsx +20 -0
- package/src/layouts/mdi-main/index.ts +27 -0
- package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
- package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
- package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
- package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
- package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
- package/src/layouts/mdi-main/mdi-main.tsx +241 -0
- package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
- package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
- package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
- package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
- package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
- package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
- package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
- package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
- package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
- package/src/layouts/mdi-main/mdi-types.ts +131 -0
- package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
- package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
- package/src/layouts/site-header/announcement-bar.tsx +55 -0
- package/src/layouts/site-header/category-strip.tsx +41 -0
- package/src/layouts/site-header/index.ts +16 -0
- package/src/layouts/site-header/main-navbar.tsx +179 -0
- package/src/layouts/site-header/mobile-drawer.tsx +140 -0
- package/src/layouts/site-header/site-header-types.ts +63 -0
- package/src/layouts/site-header/site-header.tsx +58 -0
- package/src/lib/random-id.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/pdf-provider.ts +137 -0
- package/src/styles/globals.css +374 -0
- package/src/templates/README.md +12 -0
- package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
- package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
- package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
- package/src/templates/dashboard-01/components/data-table.tsx +577 -0
- package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
- package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
- package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
- package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
- package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
- package/src/templates/dashboard-01/components/site-header.tsx +14 -0
- package/src/templates/dashboard-01/data.json +614 -0
- package/src/templates/dashboard-01/index.ts +7 -0
- package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
- package/src/templates/documentation-sidebar/index.ts +2 -0
- package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
- package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
- package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
- package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
- package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
- package/src/templates/sidebar-07/index.ts +1 -0
- package/src/templates/ui-page/index.ts +7 -0
- package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
- package/src/templates/ui-page/ui-template-code.tsx +48 -0
- package/src/templates/ui-page/ui-template-header.tsx +87 -0
- package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
- package/src/templates/ui-page/ui-template-page.tsx +97 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-config.ts +30 -0
- package/src/theme/theme-provider.tsx +142 -0
- package/src/theme/theme-selector.tsx +75 -0
- package/src/tokens/index.ts +15 -0
- 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,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
|
+
}
|