hey-pharmacist-ecommerce 1.1.29 → 1.1.31
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 +10957 -1331
- package/dist/index.d.ts +10957 -1331
- package/dist/index.js +12364 -5144
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9353 -2205
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/components/AccountReviewsTab.tsx +97 -0
- package/src/components/CouponCodeInput.tsx +190 -0
- package/src/components/Header.tsx +5 -1
- package/src/components/Notification.tsx +1 -1
- package/src/components/NotificationBell.tsx +33 -0
- package/src/components/NotificationCard.tsx +211 -0
- package/src/components/NotificationDrawer.tsx +195 -0
- package/src/components/OrderCard.tsx +164 -99
- package/src/components/ProductReviewsSection.tsx +30 -0
- package/src/components/RatingDistribution.tsx +86 -0
- package/src/components/ReviewCard.tsx +59 -0
- package/src/components/ReviewForm.tsx +207 -0
- package/src/components/ReviewPromptBanner.tsx +98 -0
- package/src/components/ReviewsList.tsx +151 -0
- package/src/components/StarRating.tsx +98 -0
- package/src/hooks/useDiscounts.ts +7 -0
- package/src/hooks/useOrders.ts +15 -0
- package/src/hooks/useReviews.ts +230 -0
- package/src/hooks/useStoreCapabilities.ts +87 -0
- package/src/index.ts +29 -0
- package/src/lib/Apis/apis/auth-api.ts +19 -7
- package/src/lib/Apis/apis/categories-api.ts +97 -0
- package/src/lib/Apis/apis/discounts-api.ts +23 -72
- package/src/lib/Apis/apis/notifications-api.ts +196 -231
- package/src/lib/Apis/apis/products-api.ts +181 -0
- package/src/lib/Apis/apis/review-api.ts +283 -4
- package/src/lib/Apis/apis/shipping-api.ts +105 -0
- package/src/lib/Apis/apis/stores-api.ts +536 -0
- package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
- package/src/lib/Apis/apis/users-api.ts +8 -8
- package/src/lib/Apis/models/address-created-request.ts +0 -12
- package/src/lib/Apis/models/address.ts +0 -12
- package/src/lib/Apis/models/api-key-info-dto.ts +49 -0
- package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
- package/src/lib/Apis/models/cart-body-populated.ts +3 -3
- package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
- package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
- package/src/lib/Apis/models/create-address-dto.ts +0 -12
- package/src/lib/Apis/models/create-discount-dto.ts +31 -100
- package/src/lib/Apis/models/create-review-dto.ts +4 -4
- package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
- package/src/lib/Apis/models/create-store-address-dto.ts +0 -12
- package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
- package/src/lib/Apis/models/create-store-dto.ts +13 -0
- package/src/lib/Apis/models/create-variant-dto.ts +0 -6
- package/src/lib/Apis/models/discount.ts +37 -106
- package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
- package/src/lib/Apis/models/index.ts +24 -7
- package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
- package/src/lib/Apis/models/manual-order-dto.ts +3 -3
- package/src/lib/Apis/models/populated-discount.ts +41 -109
- package/src/lib/Apis/models/preference-update-item.ts +59 -0
- package/src/lib/Apis/models/product-light-dto.ts +40 -0
- package/src/lib/Apis/models/product-variant.ts +0 -6
- package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
- package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
- package/src/lib/Apis/models/{check-notifications-response-dto.ts → reorder-products-success-response-dto.ts} +7 -7
- package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
- package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
- package/src/lib/Apis/models/review-status-dto.ts +34 -0
- package/src/lib/Apis/models/review.ts +9 -3
- package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
- package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
- package/src/lib/Apis/models/shipment-with-order.ts +18 -0
- package/src/lib/Apis/models/shipment.ts +18 -0
- package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
- package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
- package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
- package/src/lib/Apis/models/store-entity.ts +13 -0
- package/src/lib/Apis/models/store.ts +13 -0
- package/src/lib/Apis/models/update-address-dto.ts +0 -12
- package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
- package/src/lib/Apis/models/update-discount-dto.ts +31 -100
- package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
- package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
- package/src/lib/Apis/models/update-review-dto.ts +4 -4
- package/src/lib/Apis/models/update-store-dto.ts +13 -0
- package/src/lib/Apis/models/update-variant-dto.ts +0 -6
- package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
- package/src/lib/utils/discount.ts +155 -0
- package/src/lib/validations/discount.ts +11 -0
- package/src/providers/CartProvider.tsx +2 -2
- package/src/providers/DiscountProvider.tsx +97 -0
- package/src/providers/EcommerceProvider.tsx +13 -5
- package/src/providers/NotificationCenterProvider.tsx +436 -0
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/CheckoutScreen.tsx +402 -290
- package/src/screens/NotificationSettingsScreen.tsx +413 -0
- package/src/screens/OrderDetailScreen.tsx +283 -0
- package/src/screens/OrderReviewsScreen.tsx +308 -0
- package/src/screens/OrdersScreen.tsx +31 -7
- package/src/screens/ProductDetailScreen.tsx +24 -11
- package/src/screens/ProfileScreen.tsx +5 -0
- package/src/screens/ResetPasswordScreen.tsx +10 -4
- package/src/lib/Apis/models/create-notification-dto.ts +0 -75
- package/src/lib/Apis/models/notification.ts +0 -93
- package/src/lib/Apis/models/single-notification-dto.ts +0 -99
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useForm } from 'react-hook-form';
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { StarRating } from './StarRating';
|
|
8
|
+
import { useCreateReview } from '@/hooks/useReviews';
|
|
9
|
+
import { useNotification } from '@/providers/NotificationProvider';
|
|
10
|
+
import { Loader2 } from 'lucide-react';
|
|
11
|
+
|
|
12
|
+
const reviewSchema = z.object({
|
|
13
|
+
rating: z.number().min(1, 'Please select a rating').max(5),
|
|
14
|
+
reviewType: z.string().optional(),
|
|
15
|
+
review: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(10, 'Review must be at least 10 characters')
|
|
18
|
+
.max(1000, 'Review must be less than 1000 characters'),
|
|
19
|
+
productId: z.string().optional(), // Made optional since backend API says it's optional
|
|
20
|
+
productVariantId: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
type ReviewFormData = z.infer<typeof reviewSchema>;
|
|
24
|
+
|
|
25
|
+
interface ReviewFormProps {
|
|
26
|
+
productId: string;
|
|
27
|
+
productVariantId?: string;
|
|
28
|
+
orderId: string;
|
|
29
|
+
onSuccess?: () => void;
|
|
30
|
+
onCancel?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ReviewForm({
|
|
34
|
+
productId,
|
|
35
|
+
productVariantId,
|
|
36
|
+
orderId,
|
|
37
|
+
onSuccess,
|
|
38
|
+
onCancel,
|
|
39
|
+
}: ReviewFormProps) {
|
|
40
|
+
const [rating, setRating] = useState(0);
|
|
41
|
+
const { createReview, isLoading } = useCreateReview();
|
|
42
|
+
const notification = useNotification();
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
register,
|
|
46
|
+
handleSubmit,
|
|
47
|
+
formState: { errors },
|
|
48
|
+
setValue,
|
|
49
|
+
reset,
|
|
50
|
+
} = useForm<ReviewFormData>({
|
|
51
|
+
resolver: zodResolver(reviewSchema),
|
|
52
|
+
defaultValues: {
|
|
53
|
+
productId,
|
|
54
|
+
productVariantId,
|
|
55
|
+
rating: 0,
|
|
56
|
+
reviewType: 'Product Review',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const onSubmit = async (data: ReviewFormData) => {
|
|
61
|
+
try {
|
|
62
|
+
const reviewPayload: any = {
|
|
63
|
+
rating: data.rating,
|
|
64
|
+
review: data.review,
|
|
65
|
+
reviewType: data.reviewType || 'Product Review',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Only include productId if it's not empty
|
|
69
|
+
if (data.productId) {
|
|
70
|
+
reviewPayload.productId = data.productId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Only include productVariantId if it's not empty
|
|
74
|
+
if (data.productVariantId) {
|
|
75
|
+
reviewPayload.productVariantId = data.productVariantId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('Submitting review with orderId:', orderId);
|
|
79
|
+
console.log('Submitting review with payload:', reviewPayload);
|
|
80
|
+
|
|
81
|
+
await createReview(orderId, reviewPayload);
|
|
82
|
+
|
|
83
|
+
notification.success('Review submitted', 'Thank you for your feedback!');
|
|
84
|
+
reset();
|
|
85
|
+
setRating(0);
|
|
86
|
+
onSuccess?.();
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
const errorMessage = error.response?.data?.message || 'Failed to submit review';
|
|
89
|
+
|
|
90
|
+
console.error('=== REVIEW SUBMISSION ERROR ===');
|
|
91
|
+
console.error('Status:', error.response?.status);
|
|
92
|
+
console.error('Error message:', errorMessage);
|
|
93
|
+
console.error('Full error response:', JSON.stringify(error.response?.data, null, 2));
|
|
94
|
+
console.error('Request URL:', error.config?.url);
|
|
95
|
+
console.error('Request method:', error.config?.method);
|
|
96
|
+
console.error('Request data:', error.config?.data);
|
|
97
|
+
|
|
98
|
+
if (error.response?.status === 403) {
|
|
99
|
+
notification.error('Cannot submit review', 'You can only review your own completed orders. Please refresh the page and try again.');
|
|
100
|
+
} else if (error.response?.status === 400) {
|
|
101
|
+
notification.error('Invalid review', errorMessage);
|
|
102
|
+
} else {
|
|
103
|
+
notification.error('Submission failed', errorMessage);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleRatingChange = (newRating: number) => {
|
|
109
|
+
setRating(newRating);
|
|
110
|
+
setValue('rating', newRating, { shouldValidate: true });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
115
|
+
<div>
|
|
116
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
117
|
+
Your Rating <span className="text-red-500">*</span>
|
|
118
|
+
</label>
|
|
119
|
+
<div className="flex items-center gap-3">
|
|
120
|
+
<StarRating
|
|
121
|
+
rating={rating}
|
|
122
|
+
size="lg"
|
|
123
|
+
interactive
|
|
124
|
+
onChange={handleRatingChange}
|
|
125
|
+
/>
|
|
126
|
+
{rating > 0 && (
|
|
127
|
+
<span className="text-sm text-gray-600">
|
|
128
|
+
{rating === 1 && 'Poor'}
|
|
129
|
+
{rating === 2 && 'Fair'}
|
|
130
|
+
{rating === 3 && 'Good'}
|
|
131
|
+
{rating === 4 && 'Very Good'}
|
|
132
|
+
{rating === 5 && 'Excellent'}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
{errors.rating && (
|
|
137
|
+
<p className="text-red-500 text-xs mt-1">{errors.rating.message}</p>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div>
|
|
142
|
+
<label
|
|
143
|
+
htmlFor="reviewType"
|
|
144
|
+
className="block text-sm font-medium text-gray-700 mb-2"
|
|
145
|
+
>
|
|
146
|
+
Review Type
|
|
147
|
+
</label>
|
|
148
|
+
<select
|
|
149
|
+
id="reviewType"
|
|
150
|
+
{...register('reviewType')}
|
|
151
|
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#E67E50] focus:border-transparent"
|
|
152
|
+
>
|
|
153
|
+
<option value="Product Review">Product Review</option>
|
|
154
|
+
<option value="Quality Review">Quality Review</option>
|
|
155
|
+
<option value="Service Review">Service Review</option>
|
|
156
|
+
<option value="Delivery Review">Delivery Review</option>
|
|
157
|
+
</select>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div>
|
|
161
|
+
<label
|
|
162
|
+
htmlFor="review"
|
|
163
|
+
className="block text-sm font-medium text-gray-700 mb-2"
|
|
164
|
+
>
|
|
165
|
+
Your Review <span className="text-red-500">*</span>
|
|
166
|
+
</label>
|
|
167
|
+
<textarea
|
|
168
|
+
id="review"
|
|
169
|
+
{...register('review')}
|
|
170
|
+
rows={5}
|
|
171
|
+
placeholder="Share your experience with this product..."
|
|
172
|
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#E67E50] focus:border-transparent resize-none"
|
|
173
|
+
/>
|
|
174
|
+
{errors.review && (
|
|
175
|
+
<p className="text-red-500 text-xs mt-1">{errors.review.message}</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div className="flex items-center gap-3">
|
|
180
|
+
<button
|
|
181
|
+
type="submit"
|
|
182
|
+
disabled={isLoading}
|
|
183
|
+
className="flex-1 bg-[#E67E50] text-white py-3 px-6 rounded-lg font-medium hover:bg-[#d66f40] transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
184
|
+
>
|
|
185
|
+
{isLoading ? (
|
|
186
|
+
<>
|
|
187
|
+
<Loader2 className="size-4 animate-spin" />
|
|
188
|
+
Submitting...
|
|
189
|
+
</>
|
|
190
|
+
) : (
|
|
191
|
+
'Submit Review'
|
|
192
|
+
)}
|
|
193
|
+
</button>
|
|
194
|
+
{onCancel && (
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={onCancel}
|
|
198
|
+
disabled={isLoading}
|
|
199
|
+
className="px-6 py-3 border border-gray-300 rounded-lg font-medium hover:bg-gray-50 transition-colors disabled:opacity-50"
|
|
200
|
+
>
|
|
201
|
+
Cancel
|
|
202
|
+
</button>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</form>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { Star, X } from 'lucide-react';
|
|
5
|
+
import { useRouter } from 'next/navigation';
|
|
6
|
+
import { useBasePath } from '@/providers/BasePathProvider';
|
|
7
|
+
|
|
8
|
+
interface ReviewPromptBannerProps {
|
|
9
|
+
orderId: string;
|
|
10
|
+
orderNumber?: string;
|
|
11
|
+
itemCount: number;
|
|
12
|
+
onDismiss?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ReviewPromptBanner({
|
|
16
|
+
orderId,
|
|
17
|
+
orderNumber,
|
|
18
|
+
itemCount,
|
|
19
|
+
onDismiss,
|
|
20
|
+
}: ReviewPromptBannerProps) {
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const { buildPath } = useBasePath();
|
|
23
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
24
|
+
|
|
25
|
+
// Check if this prompt has been dismissed before
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const dismissedPrompts = JSON.parse(
|
|
28
|
+
localStorage.getItem('dismissedReviewPrompts') || '[]'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!dismissedPrompts.includes(orderId)) {
|
|
32
|
+
setIsVisible(true);
|
|
33
|
+
}
|
|
34
|
+
}, [orderId]);
|
|
35
|
+
|
|
36
|
+
const handleDismiss = () => {
|
|
37
|
+
// Save to localStorage that this prompt was dismissed
|
|
38
|
+
const dismissedPrompts = JSON.parse(
|
|
39
|
+
localStorage.getItem('dismissedReviewPrompts') || '[]'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!dismissedPrompts.includes(orderId)) {
|
|
43
|
+
dismissedPrompts.push(orderId);
|
|
44
|
+
localStorage.setItem('dismissedReviewPrompts', JSON.stringify(dismissedPrompts));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setIsVisible(false);
|
|
48
|
+
onDismiss?.();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleReviewClick = () => {
|
|
52
|
+
router.push(buildPath('/reviews'));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (!isVisible) return null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="bg-gradient-to-r from-[#E67E50]/10 to-[#E67E50]/5 border border-[#E67E50]/20 rounded-lg p-4 mb-4">
|
|
59
|
+
<div className="flex items-start gap-4">
|
|
60
|
+
<div className="flex-shrink-0">
|
|
61
|
+
<div className="size-10 rounded-full bg-[#E67E50]/20 flex items-center justify-center">
|
|
62
|
+
<Star className="size-5 text-[#E67E50]" />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="flex-1 min-w-0">
|
|
67
|
+
<div className="flex items-start justify-between gap-4">
|
|
68
|
+
<div>
|
|
69
|
+
<h3 className="font-semibold text-gray-900 mb-1">
|
|
70
|
+
How was your order?
|
|
71
|
+
</h3>
|
|
72
|
+
<p className="text-sm text-gray-600">
|
|
73
|
+
{orderNumber ? `Order #${orderNumber}` : 'Your order'} has been delivered.
|
|
74
|
+
Share your experience with {itemCount} {itemCount === 1 ? 'item' : 'items'}.
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
onClick={handleDismiss}
|
|
80
|
+
className="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors"
|
|
81
|
+
aria-label="Dismiss"
|
|
82
|
+
>
|
|
83
|
+
<X className="size-5" />
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<button
|
|
88
|
+
onClick={handleReviewClick}
|
|
89
|
+
className="mt-3 inline-flex items-center gap-2 px-4 py-2 bg-[#E67E50] text-white text-sm font-medium rounded-lg hover:bg-[#d66f40] transition-colors"
|
|
90
|
+
>
|
|
91
|
+
<Star className="size-4" />
|
|
92
|
+
Write a Review
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Review } from '@/lib/Apis/models';
|
|
5
|
+
import { ReviewCard } from './ReviewCard';
|
|
6
|
+
import { Filter, Star } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
interface ReviewsListProps {
|
|
9
|
+
reviews: Review[];
|
|
10
|
+
isLoading?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type FilterOption = 'all' | 1 | 2 | 3 | 4 | 5;
|
|
14
|
+
type SortOption = 'recent' | 'oldest' | 'highest' | 'lowest';
|
|
15
|
+
|
|
16
|
+
export function ReviewsList({ reviews, isLoading }: ReviewsListProps) {
|
|
17
|
+
const [filterRating, setFilterRating] = useState<FilterOption>('all');
|
|
18
|
+
const [sortBy, setSortBy] = useState<SortOption>('recent');
|
|
19
|
+
|
|
20
|
+
const filteredReviews = React.useMemo(() => {
|
|
21
|
+
let filtered = [...reviews];
|
|
22
|
+
|
|
23
|
+
if (filterRating !== 'all') {
|
|
24
|
+
filtered = filtered.filter((review) => Math.floor(review.rating) === filterRating);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
filtered.sort((a, b) => {
|
|
28
|
+
switch (sortBy) {
|
|
29
|
+
case 'recent':
|
|
30
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
31
|
+
case 'oldest':
|
|
32
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
33
|
+
case 'highest':
|
|
34
|
+
return b.rating - a.rating;
|
|
35
|
+
case 'lowest':
|
|
36
|
+
return a.rating - b.rating;
|
|
37
|
+
default:
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return filtered;
|
|
43
|
+
}, [reviews, filterRating, sortBy]);
|
|
44
|
+
|
|
45
|
+
if (isLoading) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-4">
|
|
48
|
+
{[1, 2, 3].map((i) => (
|
|
49
|
+
<div
|
|
50
|
+
key={i}
|
|
51
|
+
className="border border-gray-200 rounded-lg p-4 bg-white animate-pulse"
|
|
52
|
+
>
|
|
53
|
+
<div className="flex items-start justify-between mb-3">
|
|
54
|
+
<div className="flex items-center gap-3">
|
|
55
|
+
<div className="w-10 h-10 bg-gray-200 rounded-full" />
|
|
56
|
+
<div>
|
|
57
|
+
<div className="h-4 w-24 bg-gray-200 rounded mb-2" />
|
|
58
|
+
<div className="h-3 w-16 bg-gray-200 rounded" />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="h-4 w-20 bg-gray-200 rounded" />
|
|
62
|
+
</div>
|
|
63
|
+
<div className="space-y-2">
|
|
64
|
+
<div className="h-3 w-full bg-gray-200 rounded" />
|
|
65
|
+
<div className="h-3 w-3/4 bg-gray-200 rounded" />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="space-y-4">
|
|
75
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 bg-white p-4 rounded-lg border border-gray-200">
|
|
76
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
77
|
+
<Filter className="size-5 text-gray-600" />
|
|
78
|
+
<span className="text-sm font-medium text-gray-700">Filter by rating:</span>
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<button
|
|
81
|
+
onClick={() => setFilterRating('all')}
|
|
82
|
+
className={`px-3 py-1 text-sm rounded-full transition-colors ${
|
|
83
|
+
filterRating === 'all'
|
|
84
|
+
? 'bg-[#E67E50] text-white'
|
|
85
|
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
All
|
|
89
|
+
</button>
|
|
90
|
+
{[5, 4, 3, 2, 1].map((rating) => (
|
|
91
|
+
<button
|
|
92
|
+
key={rating}
|
|
93
|
+
onClick={() => setFilterRating(rating as FilterOption)}
|
|
94
|
+
className={`flex items-center gap-1 px-3 py-1 text-sm rounded-full transition-colors ${
|
|
95
|
+
filterRating === rating
|
|
96
|
+
? 'bg-[#E67E50] text-white'
|
|
97
|
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
{rating}
|
|
101
|
+
<Star className="size-3 fill-current" />
|
|
102
|
+
</button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<span className="text-sm font-medium text-gray-700">Sort by:</span>
|
|
109
|
+
<select
|
|
110
|
+
value={sortBy}
|
|
111
|
+
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
|
112
|
+
className="px-3 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#E67E50] focus:border-transparent"
|
|
113
|
+
>
|
|
114
|
+
<option value="recent">Most Recent</option>
|
|
115
|
+
<option value="oldest">Oldest First</option>
|
|
116
|
+
<option value="highest">Highest Rating</option>
|
|
117
|
+
<option value="lowest">Lowest Rating</option>
|
|
118
|
+
</select>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{filteredReviews.length === 0 ? (
|
|
123
|
+
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
|
|
124
|
+
<Star className="size-12 text-gray-300 mx-auto mb-4" />
|
|
125
|
+
<p className="text-gray-600 font-medium mb-1">
|
|
126
|
+
{filterRating === 'all' ? 'No reviews yet' : `No ${filterRating}-star reviews yet`}
|
|
127
|
+
</p>
|
|
128
|
+
<p className="text-sm text-gray-500">
|
|
129
|
+
{filterRating === 'all'
|
|
130
|
+
? 'Be the first to review this product!'
|
|
131
|
+
: 'Try selecting a different rating filter'}
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
<div className="space-y-4">
|
|
136
|
+
{filteredReviews.map((review) => (
|
|
137
|
+
<ReviewCard key={review.id || review._id} review={review} />
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{filteredReviews.length > 0 && (
|
|
143
|
+
<div className="text-center py-4">
|
|
144
|
+
<p className="text-sm text-gray-600">
|
|
145
|
+
Showing {filteredReviews.length} of {reviews.length} reviews
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Star } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface StarRatingProps {
|
|
7
|
+
rating: number;
|
|
8
|
+
maxRating?: number;
|
|
9
|
+
size?: 'sm' | 'md' | 'lg';
|
|
10
|
+
showNumber?: boolean;
|
|
11
|
+
interactive?: boolean;
|
|
12
|
+
onChange?: (rating: number) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sizeClasses = {
|
|
17
|
+
sm: 'size-3',
|
|
18
|
+
md: 'size-5',
|
|
19
|
+
lg: 'size-6',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function StarRating({
|
|
23
|
+
rating,
|
|
24
|
+
maxRating = 5,
|
|
25
|
+
size = 'md',
|
|
26
|
+
showNumber = false,
|
|
27
|
+
interactive = false,
|
|
28
|
+
onChange,
|
|
29
|
+
className,
|
|
30
|
+
}: StarRatingProps) {
|
|
31
|
+
const [hoverRating, setHoverRating] = React.useState(0);
|
|
32
|
+
|
|
33
|
+
const handleClick = (index: number) => {
|
|
34
|
+
if (interactive && onChange) {
|
|
35
|
+
onChange(index + 1);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleMouseEnter = (index: number) => {
|
|
40
|
+
if (interactive) {
|
|
41
|
+
setHoverRating(index + 1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleMouseLeave = () => {
|
|
46
|
+
if (interactive) {
|
|
47
|
+
setHoverRating(0);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const displayRating = hoverRating || rating;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={'flex items-center gap-1'}>
|
|
55
|
+
<div className="flex items-center">
|
|
56
|
+
{Array.from({ length: maxRating }).map((_, index) => {
|
|
57
|
+
const isFilled = index < Math.floor(displayRating);
|
|
58
|
+
const isHalfFilled = index === Math.floor(displayRating) && displayRating % 1 !== 0;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
key={index}
|
|
63
|
+
type="button"
|
|
64
|
+
disabled={!interactive}
|
|
65
|
+
onClick={() => handleClick(index)}
|
|
66
|
+
onMouseEnter={() => handleMouseEnter(index)}
|
|
67
|
+
onMouseLeave={handleMouseLeave}
|
|
68
|
+
className={`transition-all ${interactive && 'cursor-pointer hover:scale-110'} ${!interactive && 'cursor-default'
|
|
69
|
+
}`}
|
|
70
|
+
>
|
|
71
|
+
{isHalfFilled ? (
|
|
72
|
+
<div className="relative">
|
|
73
|
+
<Star className={`${sizeClasses[size]} text-gray-300`} />
|
|
74
|
+
<div className="absolute inset-0 overflow-hidden" style={{ width: '50%' }}>
|
|
75
|
+
<Star className={`${sizeClasses[size]} fill-[#E67E50] text-[#E67E50]`} />
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
) : (
|
|
79
|
+
<Star
|
|
80
|
+
className={`
|
|
81
|
+
${sizeClasses[size]}
|
|
82
|
+
${isFilled ? 'fill-[#E67E50] text-[#E67E50]' : 'text-gray-300'}
|
|
83
|
+
${interactive && hoverRating > 0 && index < hoverRating ? 'fill-[#E67E50] text-[#E67E50]' : ''}
|
|
84
|
+
`}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
</button>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
{showNumber && (
|
|
92
|
+
<span className="text-sm text-gray-600 ml-1">
|
|
93
|
+
{rating.toFixed(1)}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
package/src/hooks/useOrders.ts
CHANGED
|
@@ -54,12 +54,27 @@ export function useOrders(
|
|
|
54
54
|
fetchOrders();
|
|
55
55
|
}, [fetchOrders]);
|
|
56
56
|
|
|
57
|
+
const deleteOrder = async (id: string) => {
|
|
58
|
+
try {
|
|
59
|
+
await new OrdersApi(getApiConfiguration()).deleteOrder(id);
|
|
60
|
+
// Remove from local state to give immediate feedback
|
|
61
|
+
setOrders((prev) => prev.filter((o) => (o._id || o.id) !== id));
|
|
62
|
+
// Optionally refetch to ensure pagination is correct
|
|
63
|
+
// fetchOrders();
|
|
64
|
+
return true;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('Failed to delete order:', err);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
57
71
|
return {
|
|
58
72
|
orders,
|
|
59
73
|
isLoading,
|
|
60
74
|
error,
|
|
61
75
|
pagination,
|
|
62
76
|
refetch: fetchOrders,
|
|
77
|
+
deleteOrder,
|
|
63
78
|
};
|
|
64
79
|
}
|
|
65
80
|
|