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.
Files changed (104) hide show
  1. package/dist/index.d.mts +10957 -1331
  2. package/dist/index.d.ts +10957 -1331
  3. package/dist/index.js +12364 -5144
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +9353 -2205
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +4 -3
  8. package/src/components/AccountReviewsTab.tsx +97 -0
  9. package/src/components/CouponCodeInput.tsx +190 -0
  10. package/src/components/Header.tsx +5 -1
  11. package/src/components/Notification.tsx +1 -1
  12. package/src/components/NotificationBell.tsx +33 -0
  13. package/src/components/NotificationCard.tsx +211 -0
  14. package/src/components/NotificationDrawer.tsx +195 -0
  15. package/src/components/OrderCard.tsx +164 -99
  16. package/src/components/ProductReviewsSection.tsx +30 -0
  17. package/src/components/RatingDistribution.tsx +86 -0
  18. package/src/components/ReviewCard.tsx +59 -0
  19. package/src/components/ReviewForm.tsx +207 -0
  20. package/src/components/ReviewPromptBanner.tsx +98 -0
  21. package/src/components/ReviewsList.tsx +151 -0
  22. package/src/components/StarRating.tsx +98 -0
  23. package/src/hooks/useDiscounts.ts +7 -0
  24. package/src/hooks/useOrders.ts +15 -0
  25. package/src/hooks/useReviews.ts +230 -0
  26. package/src/hooks/useStoreCapabilities.ts +87 -0
  27. package/src/index.ts +29 -0
  28. package/src/lib/Apis/apis/auth-api.ts +19 -7
  29. package/src/lib/Apis/apis/categories-api.ts +97 -0
  30. package/src/lib/Apis/apis/discounts-api.ts +23 -72
  31. package/src/lib/Apis/apis/notifications-api.ts +196 -231
  32. package/src/lib/Apis/apis/products-api.ts +181 -0
  33. package/src/lib/Apis/apis/review-api.ts +283 -4
  34. package/src/lib/Apis/apis/shipping-api.ts +105 -0
  35. package/src/lib/Apis/apis/stores-api.ts +536 -0
  36. package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
  37. package/src/lib/Apis/apis/users-api.ts +8 -8
  38. package/src/lib/Apis/models/address-created-request.ts +0 -12
  39. package/src/lib/Apis/models/address.ts +0 -12
  40. package/src/lib/Apis/models/api-key-info-dto.ts +49 -0
  41. package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
  42. package/src/lib/Apis/models/cart-body-populated.ts +3 -3
  43. package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
  44. package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
  45. package/src/lib/Apis/models/create-address-dto.ts +0 -12
  46. package/src/lib/Apis/models/create-discount-dto.ts +31 -100
  47. package/src/lib/Apis/models/create-review-dto.ts +4 -4
  48. package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
  49. package/src/lib/Apis/models/create-store-address-dto.ts +0 -12
  50. package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
  51. package/src/lib/Apis/models/create-store-dto.ts +13 -0
  52. package/src/lib/Apis/models/create-variant-dto.ts +0 -6
  53. package/src/lib/Apis/models/discount.ts +37 -106
  54. package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
  55. package/src/lib/Apis/models/index.ts +24 -7
  56. package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
  57. package/src/lib/Apis/models/manual-order-dto.ts +3 -3
  58. package/src/lib/Apis/models/populated-discount.ts +41 -109
  59. package/src/lib/Apis/models/preference-update-item.ts +59 -0
  60. package/src/lib/Apis/models/product-light-dto.ts +40 -0
  61. package/src/lib/Apis/models/product-variant.ts +0 -6
  62. package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
  63. package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
  64. package/src/lib/Apis/models/{check-notifications-response-dto.ts → reorder-products-success-response-dto.ts} +7 -7
  65. package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
  66. package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
  67. package/src/lib/Apis/models/review-status-dto.ts +34 -0
  68. package/src/lib/Apis/models/review.ts +9 -3
  69. package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
  70. package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
  71. package/src/lib/Apis/models/shipment-with-order.ts +18 -0
  72. package/src/lib/Apis/models/shipment.ts +18 -0
  73. package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
  74. package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
  75. package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
  76. package/src/lib/Apis/models/store-entity.ts +13 -0
  77. package/src/lib/Apis/models/store.ts +13 -0
  78. package/src/lib/Apis/models/update-address-dto.ts +0 -12
  79. package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
  80. package/src/lib/Apis/models/update-discount-dto.ts +31 -100
  81. package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
  82. package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
  83. package/src/lib/Apis/models/update-review-dto.ts +4 -4
  84. package/src/lib/Apis/models/update-store-dto.ts +13 -0
  85. package/src/lib/Apis/models/update-variant-dto.ts +0 -6
  86. package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
  87. package/src/lib/utils/discount.ts +155 -0
  88. package/src/lib/validations/discount.ts +11 -0
  89. package/src/providers/CartProvider.tsx +2 -2
  90. package/src/providers/DiscountProvider.tsx +97 -0
  91. package/src/providers/EcommerceProvider.tsx +13 -5
  92. package/src/providers/NotificationCenterProvider.tsx +436 -0
  93. package/src/screens/CartScreen.tsx +1 -1
  94. package/src/screens/CheckoutScreen.tsx +402 -290
  95. package/src/screens/NotificationSettingsScreen.tsx +413 -0
  96. package/src/screens/OrderDetailScreen.tsx +283 -0
  97. package/src/screens/OrderReviewsScreen.tsx +308 -0
  98. package/src/screens/OrdersScreen.tsx +31 -7
  99. package/src/screens/ProductDetailScreen.tsx +24 -11
  100. package/src/screens/ProfileScreen.tsx +5 -0
  101. package/src/screens/ResetPasswordScreen.tsx +10 -4
  102. package/src/lib/Apis/models/create-notification-dto.ts +0 -75
  103. package/src/lib/Apis/models/notification.ts +0 -93
  104. 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
+ }
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+
3
+ import { useDiscountsContext } from '@/providers/DiscountProvider';
4
+
5
+ export function useDiscounts() {
6
+ return useDiscountsContext();
7
+ }
@@ -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