hey-pharmacist-ecommerce 1.1.11 → 1.1.13
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/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +632 -511
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +633 -512
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CartItem.tsx +63 -42
- package/src/components/FilterChips.tsx +54 -80
- package/src/components/OrderCard.tsx +89 -56
- package/src/components/ProductCard.tsx +131 -55
- package/src/hooks/useOrders.ts +1 -0
- package/src/lib/types/index.ts +1 -0
- package/src/providers/CartProvider.tsx +47 -3
- package/src/screens/CartScreen.tsx +146 -231
- package/src/screens/CheckoutScreen.tsx +30 -61
- package/src/screens/LoginScreen.tsx +1 -1
- package/src/screens/OrdersScreen.tsx +91 -148
- package/src/screens/ProductDetailScreen.tsx +355 -362
- package/src/screens/RegisterScreen.tsx +1 -1
- package/src/screens/ShopScreen.tsx +439 -268
- package/src/screens/WishlistScreen.tsx +80 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hey-pharmacist-ecommerce",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "Production-ready, multi-tenant e‑commerce UI + API adapter for Next.js with auth, carts, checkout, orders, theming, and pharmacist-focused UX.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -32,6 +32,18 @@ export function CartItem({ item }: CartItemProps) {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const itemTotal = item.productVariantData.finalPrice * item.quantity;
|
|
35
|
+
const unitPrice = item.productVariantData.finalPrice;
|
|
36
|
+
|
|
37
|
+
// Extract attributes from variant
|
|
38
|
+
const attributes: string[] = [];
|
|
39
|
+
if (item.productVariantData.attribute) {
|
|
40
|
+
if (item.productVariantData.attribute.color) {
|
|
41
|
+
attributes.push(`Color: ${item.productVariantData.attribute.color}`);
|
|
42
|
+
}
|
|
43
|
+
if (item.productVariantData.attribute.size) {
|
|
44
|
+
attributes.push(`Size: ${item.productVariantData.attribute.size}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
35
47
|
|
|
36
48
|
return (
|
|
37
49
|
<motion.div
|
|
@@ -39,38 +51,52 @@ export function CartItem({ item }: CartItemProps) {
|
|
|
39
51
|
initial={{ opacity: 0, y: 20 }}
|
|
40
52
|
animate={{ opacity: 1, y: 0 }}
|
|
41
53
|
exit={{ opacity: 0, x: -100 }}
|
|
42
|
-
className="
|
|
54
|
+
className="relative bg-white rounded-lg shadow-sm border border-gray-200 p-4"
|
|
43
55
|
>
|
|
44
|
-
{/*
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
</div>
|
|
56
|
+
{/* Delete Icon - Top Right */}
|
|
57
|
+
<button
|
|
58
|
+
onClick={handleRemove}
|
|
59
|
+
className="absolute top-4 right-4 p-1 text-gray-400 hover:text-red-600 transition-colors"
|
|
60
|
+
aria-label="Remove item"
|
|
61
|
+
>
|
|
62
|
+
<Trash2 className="w-5 h-5" />
|
|
63
|
+
</button>
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
<div className="flex gap-4 pr-8">
|
|
66
|
+
{/* Product Image */}
|
|
67
|
+
<div className="relative w-20 h-24 rounded-md overflow-hidden flex-shrink-0 bg-gray-100">
|
|
68
|
+
<Image
|
|
69
|
+
src={item.productVariantData.productMedia[0]?.file || '/placeholder-product.jpg'}
|
|
70
|
+
alt={item.productVariantData.name}
|
|
71
|
+
fill
|
|
72
|
+
className="object-cover"
|
|
73
|
+
sizes="80px"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
62
76
|
|
|
63
|
-
{/*
|
|
64
|
-
<div className="flex
|
|
65
|
-
<
|
|
77
|
+
{/* Product Info */}
|
|
78
|
+
<div className="flex-1 min-w-0">
|
|
79
|
+
<h3 className="text-base font-bold text-gray-900">
|
|
80
|
+
{item.productVariantData.name}
|
|
81
|
+
</h3>
|
|
82
|
+
|
|
83
|
+
{/* Attributes */}
|
|
84
|
+
{attributes.length > 0 && (
|
|
85
|
+
<p className="text-sm text-gray-500 mt-1">
|
|
86
|
+
{attributes.join(' ')}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Quantity Controls */}
|
|
91
|
+
<div className="flex items-center gap-2 mt-3">
|
|
66
92
|
<button
|
|
67
93
|
onClick={() => handleUpdateQuantity(item.quantity - 1)}
|
|
68
|
-
disabled={isUpdating || item.quantity <=
|
|
69
|
-
className="p-
|
|
94
|
+
disabled={isUpdating || item.quantity <= 1}
|
|
95
|
+
className="p-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors rounded"
|
|
70
96
|
>
|
|
71
|
-
<Minus className="w-4 h-4" />
|
|
97
|
+
<Minus className="w-4 h-4 text-gray-600" />
|
|
72
98
|
</button>
|
|
73
|
-
<span className="px-
|
|
99
|
+
<span className="px-3 font-medium min-w-[2rem] text-center text-gray-900">
|
|
74
100
|
{isUpdating ? (
|
|
75
101
|
<span className="inline-block h-4 w-4 align-middle animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
|
|
76
102
|
) : (
|
|
@@ -79,28 +105,23 @@ export function CartItem({ item }: CartItemProps) {
|
|
|
79
105
|
</span>
|
|
80
106
|
<button
|
|
81
107
|
onClick={() => handleUpdateQuantity(item.quantity + 1)}
|
|
82
|
-
disabled={isUpdating || item.quantity >= item.productVariantData.inventoryCount}
|
|
83
|
-
className="p-
|
|
108
|
+
disabled={isUpdating || item.quantity >= (item.productVariantData.inventoryCount || 999)}
|
|
109
|
+
className="p-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors rounded"
|
|
84
110
|
>
|
|
85
|
-
<Plus className="w-4 h-4" />
|
|
111
|
+
<Plus className="w-4 h-4 text-gray-600" />
|
|
86
112
|
</button>
|
|
87
113
|
</div>
|
|
88
|
-
|
|
89
|
-
<button
|
|
90
|
-
onClick={handleRemove}
|
|
91
|
-
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
92
|
-
aria-label="Remove item"
|
|
93
|
-
>
|
|
94
|
-
<Trash2 className="w-5 h-5" />
|
|
95
|
-
</button>
|
|
96
114
|
</div>
|
|
97
|
-
</div>
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
{/* Pricing - Right Side */}
|
|
117
|
+
<div className="text-right flex-shrink-0">
|
|
118
|
+
<p className="text-lg font-bold text-orange-600">
|
|
119
|
+
{formatPrice(itemTotal)}
|
|
120
|
+
</p>
|
|
121
|
+
<p className="text-sm text-gray-500 mt-1">
|
|
122
|
+
{formatPrice(unitPrice)} each
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
104
125
|
</div>
|
|
105
126
|
</motion.div>
|
|
106
127
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
-
import { Check,
|
|
3
|
+
import { Check, ChevronDown, X } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
interface FilterChipsProps {
|
|
6
6
|
label: string;
|
|
@@ -22,11 +22,8 @@ export function FilterChips({
|
|
|
22
22
|
variant = 'primary',
|
|
23
23
|
}: FilterChipsProps) {
|
|
24
24
|
const [isOverflowOpen, setIsOverflowOpen] = useState(false);
|
|
25
|
-
const [filterSearchTerm, setFilterSearchTerm] = useState('');
|
|
26
25
|
const overflowMenuRef = useRef<HTMLDivElement | null>(null);
|
|
27
26
|
|
|
28
|
-
const color = variant === 'primary' ? 'primary' : 'secondary';
|
|
29
|
-
|
|
30
27
|
const { visibleFilters, overflowFilters } = useMemo(() => {
|
|
31
28
|
const basePrimary = filters.slice(0, maxVisible);
|
|
32
29
|
|
|
@@ -52,19 +49,6 @@ export function FilterChips({
|
|
|
52
49
|
};
|
|
53
50
|
}, [filters, maxVisible, selected]);
|
|
54
51
|
|
|
55
|
-
const filteredOverflowFilters = useMemo(() => {
|
|
56
|
-
if (!filterSearchTerm.trim()) return overflowFilters;
|
|
57
|
-
return overflowFilters.filter((filter) =>
|
|
58
|
-
filter.toLowerCase().includes(filterSearchTerm.toLowerCase())
|
|
59
|
-
);
|
|
60
|
-
}, [filterSearchTerm, overflowFilters]);
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (!isOverflowOpen) {
|
|
64
|
-
setFilterSearchTerm('');
|
|
65
|
-
}
|
|
66
|
-
}, [isOverflowOpen]);
|
|
67
|
-
|
|
68
52
|
useEffect(() => {
|
|
69
53
|
function handleClickOutside(event: MouseEvent) {
|
|
70
54
|
if (overflowMenuRef.current && !overflowMenuRef.current.contains(event.target as Node)) {
|
|
@@ -81,71 +65,76 @@ export function FilterChips({
|
|
|
81
65
|
};
|
|
82
66
|
}, [isOverflowOpen]);
|
|
83
67
|
|
|
68
|
+
const isPrimary = variant === 'primary';
|
|
69
|
+
|
|
84
70
|
return (
|
|
85
|
-
<div className="
|
|
86
|
-
<
|
|
87
|
-
<Icon className="h-4 w-4" />
|
|
88
|
-
{label}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<div className="flex items-center gap-2">
|
|
73
|
+
<Icon className="h-4 w-4 text-slate-500" />
|
|
74
|
+
<span className="text-sm font-medium text-slate-700">{label}</span>
|
|
75
|
+
{selected !== 'All' && (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => onSelect('All')}
|
|
79
|
+
className="ml-auto text-xs text-primary-600 hover:text-primary-700 font-medium"
|
|
80
|
+
>
|
|
81
|
+
Clear
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
87
|
+
{visibleFilters.map((filter) => {
|
|
88
|
+
const isSelected = selected === filter;
|
|
89
|
+
return (
|
|
93
90
|
<button
|
|
94
91
|
key={filter}
|
|
95
92
|
type="button"
|
|
96
93
|
onClick={() => onSelect(filter)}
|
|
97
|
-
className={`rounded-
|
|
98
|
-
|
|
99
|
-
?
|
|
100
|
-
|
|
94
|
+
className={`inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-all whitespace-nowrap ${
|
|
95
|
+
isSelected
|
|
96
|
+
? isPrimary
|
|
97
|
+
? 'border-primary-500 bg-primary-500 text-white shadow-sm'
|
|
98
|
+
: 'border-secondary-500 bg-secondary-500 text-white shadow-sm'
|
|
99
|
+
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50'
|
|
101
100
|
}`}
|
|
102
101
|
>
|
|
103
102
|
{filter}
|
|
103
|
+
{isSelected && <X className="h-3.5 w-3.5" />}
|
|
104
104
|
</button>
|
|
105
|
-
)
|
|
106
|
-
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
107
|
|
|
108
108
|
{overflowFilters.length > 0 && (
|
|
109
109
|
<div className="relative" ref={overflowMenuRef}>
|
|
110
110
|
<button
|
|
111
111
|
type="button"
|
|
112
112
|
onClick={() => setIsOverflowOpen((prev) => !prev)}
|
|
113
|
-
className={`flex items-center gap-
|
|
113
|
+
className={`inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-all whitespace-nowrap ${
|
|
114
114
|
overflowFilters.includes(selected)
|
|
115
|
-
?
|
|
116
|
-
|
|
115
|
+
? isPrimary
|
|
116
|
+
? 'border-primary-500 bg-primary-50 text-primary-700'
|
|
117
|
+
: 'border-secondary-500 bg-secondary-50 text-secondary-700'
|
|
118
|
+
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50'
|
|
117
119
|
}`}
|
|
118
120
|
>
|
|
119
121
|
<span>{overflowFilters.includes(selected) ? selected : 'More'}</span>
|
|
120
|
-
<
|
|
121
|
-
{overflowFilters.length}
|
|
122
|
-
</span>
|
|
122
|
+
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${isOverflowOpen ? 'rotate-180' : ''}`} />
|
|
123
123
|
</button>
|
|
124
124
|
|
|
125
125
|
<AnimatePresence>
|
|
126
126
|
{isOverflowOpen && (
|
|
127
127
|
<motion.div
|
|
128
|
-
initial={{ opacity: 0, y: 8 }}
|
|
129
|
-
animate={{ opacity: 1, y: 0 }}
|
|
130
|
-
exit={{ opacity: 0, y: 8 }}
|
|
128
|
+
initial={{ opacity: 0, y: 8, scale: 0.95 }}
|
|
129
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
130
|
+
exit={{ opacity: 0, y: 8, scale: 0.95 }}
|
|
131
131
|
transition={{ duration: 0.15 }}
|
|
132
|
-
className="absolute
|
|
132
|
+
className="absolute left-0 top-full z-50 mt-2 w-56 rounded-lg border border-slate-200 bg-white shadow-lg"
|
|
133
133
|
>
|
|
134
|
-
<div className="
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
type="text"
|
|
139
|
-
placeholder={`Search ${label.toLowerCase()}`}
|
|
140
|
-
value={filterSearchTerm}
|
|
141
|
-
onChange={(event) => setFilterSearchTerm(event.target.value)}
|
|
142
|
-
className="w-full rounded-full border border-slate-200 bg-slate-50 py-2 pl-9 pr-3 text-sm text-slate-600 outline-none transition focus:border-primary-300 focus:bg-white focus:ring-2 focus:ring-primary-200"
|
|
143
|
-
/>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
<div className="max-h-60 overflow-y-auto px-2 py-2">
|
|
147
|
-
{filteredOverflowFilters.length > 0 ? (
|
|
148
|
-
filteredOverflowFilters.map((filter) => (
|
|
134
|
+
<div className="max-h-64 overflow-y-auto p-1.5">
|
|
135
|
+
{overflowFilters.map((filter) => {
|
|
136
|
+
const isSelected = selected === filter;
|
|
137
|
+
return (
|
|
149
138
|
<button
|
|
150
139
|
key={filter}
|
|
151
140
|
type="button"
|
|
@@ -153,34 +142,19 @@ export function FilterChips({
|
|
|
153
142
|
onSelect(filter);
|
|
154
143
|
setIsOverflowOpen(false);
|
|
155
144
|
}}
|
|
156
|
-
className={`flex w-full items-center justify-between rounded-
|
|
157
|
-
|
|
158
|
-
?
|
|
159
|
-
|
|
145
|
+
className={`flex w-full items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition ${
|
|
146
|
+
isSelected
|
|
147
|
+
? isPrimary
|
|
148
|
+
? 'bg-primary-500 text-white'
|
|
149
|
+
: 'bg-secondary-500 text-white'
|
|
150
|
+
: 'text-slate-700 hover:bg-slate-100'
|
|
160
151
|
}`}
|
|
161
152
|
>
|
|
162
153
|
<span>{filter}</span>
|
|
163
|
-
{
|
|
154
|
+
{isSelected && <Check className="h-4 w-4" />}
|
|
164
155
|
</button>
|
|
165
|
-
)
|
|
166
|
-
)
|
|
167
|
-
<p className="px-3 py-4 text-sm text-slate-500">No items found.</p>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
<div className="flex items-center justify-between gap-2 border-t border-slate-100 px-4 py-3">
|
|
171
|
-
<span className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
172
|
-
Quick actions
|
|
173
|
-
</span>
|
|
174
|
-
<button
|
|
175
|
-
type="button"
|
|
176
|
-
onClick={() => {
|
|
177
|
-
onSelect('All');
|
|
178
|
-
setIsOverflowOpen(false);
|
|
179
|
-
}}
|
|
180
|
-
className="text-xs font-semibold uppercase tracking-wide text-primary-600 hover:text-primary-700"
|
|
181
|
-
>
|
|
182
|
-
Reset to All
|
|
183
|
-
</button>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
184
158
|
</div>
|
|
185
159
|
</motion.div>
|
|
186
160
|
)}
|
|
@@ -2,91 +2,124 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { motion } from 'framer-motion';
|
|
5
|
-
import {
|
|
5
|
+
import { CreditCard } from 'lucide-react';
|
|
6
6
|
import { PaymentPaymentMethodEnum, PaymentPaymentStatusEnum, PopulatedOrder } from '@/lib/Apis';
|
|
7
7
|
import { formatPrice, formatDate } from '@/lib/utils/format';
|
|
8
8
|
import { Badge } from './ui/Badge';
|
|
9
|
-
import Link from 'next/link';
|
|
10
9
|
import Image from 'next/image';
|
|
11
|
-
import { useBasePath } from '@/providers/BasePathProvider';
|
|
12
10
|
|
|
13
11
|
interface OrderCardProps {
|
|
14
12
|
order: PopulatedOrder;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
export function OrderCard({ order }: OrderCardProps) {
|
|
18
|
-
const { buildPath } = useBasePath();
|
|
19
16
|
const config = order.orderStatus;
|
|
17
|
+
const itemCount = order.items?.length || 0;
|
|
18
|
+
const showPriceBreakdown = (order.shippingCost && order.shippingCost > 0) ||
|
|
19
|
+
(order.tax && order.tax > 0) ||
|
|
20
|
+
(order.discountedAmount && order.discountedAmount > 0);
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<motion.div
|
|
23
24
|
initial={{ opacity: 0, y: 20 }}
|
|
24
25
|
animate={{ opacity: 1, y: 0 }}
|
|
25
|
-
|
|
26
|
-
className="bg-white rounded-2xl p-6 shadow-sm hover:shadow-xl transition-all duration-300 border border-gray-100"
|
|
26
|
+
className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow"
|
|
27
27
|
>
|
|
28
|
-
{/* Header */}
|
|
29
|
-
<div className="flex justify-between
|
|
30
|
-
<div>
|
|
31
|
-
<h3 className="text-
|
|
32
|
-
|
|
33
|
-
Order #{order?._id?.slice(0, 6) || ''}
|
|
28
|
+
{/* Header - Compact */}
|
|
29
|
+
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
|
|
30
|
+
<div className="flex items-center gap-3">
|
|
31
|
+
<h3 className="text-base font-bold text-slate-900">
|
|
32
|
+
Order #{order?._id?.slice(0, 8) || ''}
|
|
34
33
|
</h3>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
{formatDate(order.createdAt || new Date(), '
|
|
38
|
-
</
|
|
34
|
+
<Badge variant={config as 'success' | 'warning' | 'primary' | 'danger' | 'gray'}>{config}</Badge>
|
|
35
|
+
<span className="text-xs text-gray-500">
|
|
36
|
+
{formatDate(order.createdAt || new Date(), 'short')}
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="text-right">
|
|
40
|
+
<p className="text-lg font-bold text-slate-900">{formatPrice(order.grandTotal || 0)}</p>
|
|
41
|
+
{itemCount > 0 && (
|
|
42
|
+
<p className="text-xs text-gray-500">{itemCount} {itemCount === 1 ? 'item' : 'items'}</p>
|
|
43
|
+
)}
|
|
39
44
|
</div>
|
|
40
|
-
<Badge variant={config as 'success' | 'warning' | 'primary' | 'danger' | 'gray'}>{config}</Badge>
|
|
41
45
|
</div>
|
|
42
46
|
|
|
43
|
-
{/* Items
|
|
47
|
+
{/* Items List - Compact */}
|
|
44
48
|
<div className="space-y-2 mb-4">
|
|
45
|
-
{order.items
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
{order.items && order.items.length > 0 ? (
|
|
50
|
+
order.items.slice(0, 3).map((item) => {
|
|
51
|
+
const itemPrice = item.productVariantData?.finalPrice || 0;
|
|
52
|
+
const itemTotal = itemPrice * item.quantity;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div key={item.productVariantId || item._id} className="flex items-center gap-2 text-sm">
|
|
56
|
+
<div className="relative w-12 h-12 rounded bg-gray-100 flex-shrink-0 overflow-hidden">
|
|
57
|
+
<Image
|
|
58
|
+
src={item?.productVariantData?.productMedia?.[0]?.file || '/placeholder-product.jpg'}
|
|
59
|
+
alt={item?.productVariantData?.name || 'Product image'}
|
|
60
|
+
fill
|
|
61
|
+
className="object-cover"
|
|
62
|
+
sizes="48px"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex-1 min-w-0">
|
|
66
|
+
<p className="font-medium text-slate-900 truncate text-sm">
|
|
67
|
+
{item.productVariantData?.name || 'Unknown Product'}
|
|
68
|
+
</p>
|
|
69
|
+
<p className="text-xs text-gray-500">Qty: {item.quantity}</p>
|
|
70
|
+
</div>
|
|
71
|
+
<p className="font-semibold text-slate-900 text-sm">{formatPrice(itemTotal)}</p>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
})
|
|
75
|
+
) : (
|
|
76
|
+
<p className="text-sm text-gray-500 text-center py-2">No items found</p>
|
|
77
|
+
)}
|
|
78
|
+
{order.items && order.items.length > 3 && (
|
|
79
|
+
<p className="text-xs text-gray-500 text-center pt-1">
|
|
80
|
+
+{order.items.length - 3} more {order.items.length - 3 === 1 ? 'item' : 'items'}
|
|
58
81
|
</p>
|
|
59
82
|
)}
|
|
60
83
|
</div>
|
|
61
84
|
|
|
62
|
-
{/*
|
|
63
|
-
|
|
64
|
-
<div>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{order.payment.paymentStatus !== PaymentPaymentStatusEnum.Paid && order.payment.paymentMethod === PaymentPaymentMethodEnum.Card && (
|
|
71
|
-
<a
|
|
72
|
-
href={order?.payment?.paymentIntent?.hostedInvoiceUrl || ''}
|
|
73
|
-
target="_blank"
|
|
74
|
-
rel="noopener noreferrer"
|
|
75
|
-
className="inline-flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
|
76
|
-
>
|
|
77
|
-
<CreditCard className="w-4 h-4" />
|
|
78
|
-
Pay Now
|
|
79
|
-
</a>
|
|
85
|
+
{/* Price Breakdown - Only if needed */}
|
|
86
|
+
{showPriceBreakdown && (
|
|
87
|
+
<div className="mb-4 pb-4 border-b border-gray-200 space-y-1 text-xs">
|
|
88
|
+
{order.shippingCost !== undefined && order.shippingCost > 0 && (
|
|
89
|
+
<div className="flex justify-between text-gray-600">
|
|
90
|
+
<span>Shipping</span>
|
|
91
|
+
<span>{formatPrice(order.shippingCost)}</span>
|
|
92
|
+
</div>
|
|
80
93
|
)}
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
{order.tax !== undefined && order.tax > 0 && (
|
|
95
|
+
<div className="flex justify-between text-gray-600">
|
|
96
|
+
<span>Tax</span>
|
|
97
|
+
<span>{formatPrice(order.tax)}</span>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
{order.discountedAmount !== undefined && order.discountedAmount > 0 && (
|
|
101
|
+
<div className="flex justify-between text-green-600">
|
|
102
|
+
<span>Discount</span>
|
|
103
|
+
<span>-{formatPrice(order.discountedAmount)}</span>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{/* Footer Actions */}
|
|
110
|
+
{order.payment?.paymentStatus !== PaymentPaymentStatusEnum.Paid && order.payment?.paymentMethod === PaymentPaymentMethodEnum.Card && (
|
|
111
|
+
<div className="flex justify-end">
|
|
112
|
+
<button
|
|
113
|
+
onClick={() => {
|
|
114
|
+
window.open(order?.payment?.hostedInvoiceUrl || '', '_blank')
|
|
115
|
+
}}
|
|
116
|
+
className="inline-flex items-center gap-2 rounded-full border-2 border-primary-500 bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 text-sm font-medium transition-colors"
|
|
84
117
|
>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</
|
|
118
|
+
<CreditCard className="w-4 h-4" />
|
|
119
|
+
Pay Now
|
|
120
|
+
</button>
|
|
88
121
|
</div>
|
|
89
|
-
|
|
122
|
+
)}
|
|
90
123
|
</motion.div>
|
|
91
124
|
);
|
|
92
125
|
}
|