hey-pharmacist-ecommerce 1.0.4 → 1.0.6
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 +107 -1
- package/dist/index.d.mts +3636 -316
- package/dist/index.d.ts +3636 -316
- package/dist/index.js +6802 -3865
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6756 -3817
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -14
- package/src/components/AddressFormModal.tsx +171 -0
- package/src/components/CartItem.tsx +17 -12
- package/src/components/FilterChips.tsx +195 -0
- package/src/components/Header.tsx +121 -71
- package/src/components/OrderCard.tsx +18 -25
- package/src/components/ProductCard.tsx +209 -72
- package/src/components/ui/Button.tsx +13 -5
- package/src/components/ui/Card.tsx +46 -0
- package/src/hooks/useAddresses.ts +83 -0
- package/src/hooks/useOrders.ts +37 -19
- package/src/hooks/useProducts.ts +55 -63
- package/src/hooks/useWishlistProducts.ts +75 -0
- package/src/index.ts +3 -19
- package/src/lib/Apis/api.ts +1 -0
- package/src/lib/Apis/apis/cart-api.ts +3 -3
- package/src/lib/Apis/apis/inventory-api.ts +0 -108
- package/src/lib/Apis/apis/stores-api.ts +70 -0
- package/src/lib/Apis/apis/wishlist-api.ts +447 -0
- package/src/lib/Apis/models/cart-item-populated.ts +0 -1
- package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
- package/src/lib/Apis/models/create-variant-dto.ts +26 -33
- package/src/lib/Apis/models/extended-product-dto.ts +20 -24
- package/src/lib/Apis/models/index.ts +2 -1
- package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
- package/src/lib/Apis/models/order.ts +3 -8
- package/src/lib/Apis/models/populated-order.ts +3 -8
- package/src/lib/Apis/models/product-variant.ts +29 -0
- package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
- package/src/lib/Apis/models/wishlist.ts +51 -0
- package/src/lib/Apis/wrapper.ts +18 -7
- package/src/lib/api-adapter/index.ts +0 -12
- package/src/lib/types/index.ts +16 -61
- package/src/lib/utils/colors.ts +7 -4
- package/src/lib/utils/format.ts +1 -1
- package/src/lib/validations/address.ts +14 -0
- package/src/providers/AuthProvider.tsx +61 -31
- package/src/providers/CartProvider.tsx +18 -28
- package/src/providers/EcommerceProvider.tsx +7 -0
- package/src/providers/FavoritesProvider.tsx +86 -0
- package/src/providers/ThemeProvider.tsx +16 -1
- package/src/providers/WishlistProvider.tsx +174 -0
- package/src/screens/AddressesScreen.tsx +484 -0
- package/src/screens/CartScreen.tsx +120 -84
- package/src/screens/CategoriesScreen.tsx +120 -0
- package/src/screens/CheckoutScreen.tsx +919 -241
- package/src/screens/CurrentOrdersScreen.tsx +125 -61
- package/src/screens/HomeScreen.tsx +209 -0
- package/src/screens/LoginScreen.tsx +133 -88
- package/src/screens/NewAddressScreen.tsx +187 -0
- package/src/screens/OrdersScreen.tsx +162 -50
- package/src/screens/ProductDetailScreen.tsx +641 -190
- package/src/screens/ProfileScreen.tsx +192 -116
- package/src/screens/RegisterScreen.tsx +193 -144
- package/src/screens/SearchResultsScreen.tsx +165 -0
- package/src/screens/ShopScreen.tsx +1110 -146
- package/src/screens/WishlistScreen.tsx +428 -0
- package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
- package/src/lib/api/auth.ts +0 -81
- package/src/lib/api/cart.ts +0 -42
- package/src/lib/api/orders.ts +0 -53
- package/src/lib/api/products.ts +0 -51
- package/src/lib/api-adapter/auth-adapter.ts +0 -196
- package/src/lib/api-adapter/cart-adapter.ts +0 -193
- package/src/lib/api-adapter/mappers.ts +0 -147
- package/src/lib/api-adapter/orders-adapter.ts +0 -195
- package/src/lib/api-adapter/products-adapter.ts +0 -194
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import {
|
|
6
|
+
Grid,
|
|
7
|
+
Heart,
|
|
8
|
+
List,
|
|
9
|
+
LucideIcon,
|
|
10
|
+
Package,
|
|
11
|
+
ShieldCheck,
|
|
12
|
+
ShoppingBag,
|
|
13
|
+
Sparkles,
|
|
14
|
+
Trash2,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { useRouter } from 'next/navigation';
|
|
17
|
+
import { toast } from 'sonner';
|
|
18
|
+
import { ProductCard } from '@/components/ProductCard';
|
|
19
|
+
import { Button } from '@/components/ui/Button';
|
|
20
|
+
import { useWishlist } from '@/providers/WishlistProvider';
|
|
21
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
22
|
+
import { useWishlistProducts } from '@/hooks/useWishlistProducts';
|
|
23
|
+
import Image from 'next/image';
|
|
24
|
+
import { formatPrice } from '@/lib/utils/format';
|
|
25
|
+
import { ExtendedProductDTO } from '@/lib/Apis';
|
|
26
|
+
|
|
27
|
+
type SortOption = 'featured' | 'price-low' | 'price-high' | 'name' | 'availability';
|
|
28
|
+
|
|
29
|
+
const SORT_OPTIONS: { value: SortOption; label: string }[] = [
|
|
30
|
+
{ value: 'featured', label: 'Most loved' },
|
|
31
|
+
{ value: 'price-low', label: 'Price: Low to High' },
|
|
32
|
+
{ value: 'price-high', label: 'Price: High to Low' },
|
|
33
|
+
{ value: 'name', label: 'Name A-Z' },
|
|
34
|
+
{ value: 'availability', label: 'Availability' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
interface InsightCardProps {
|
|
38
|
+
icon: LucideIcon;
|
|
39
|
+
title: string;
|
|
40
|
+
value: string;
|
|
41
|
+
helper?: string;
|
|
42
|
+
accentClassName: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export default function WishlistScreen() {
|
|
47
|
+
const router = useRouter();
|
|
48
|
+
const { isAuthenticated } = useAuth() || {};
|
|
49
|
+
const {
|
|
50
|
+
products: wishlistItems,
|
|
51
|
+
removeFromWishlist,
|
|
52
|
+
getWishlistCount,
|
|
53
|
+
clearWishlist,
|
|
54
|
+
refreshWishlist,
|
|
55
|
+
} = useWishlist();
|
|
56
|
+
|
|
57
|
+
const wishlistCount = getWishlistCount?.() ?? 0;
|
|
58
|
+
const { products: wishlistProducts, isLoading, error } = useWishlistProducts(
|
|
59
|
+
wishlistItems as string[]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const [onlyInStock, setOnlyInStock] = useState(false);
|
|
63
|
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list');
|
|
64
|
+
const [sortOption, setSortOption] = useState<SortOption>('featured');
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (error) {
|
|
68
|
+
toast.error('We had trouble loading your saved products. Please try again.');
|
|
69
|
+
}
|
|
70
|
+
}, [error]);
|
|
71
|
+
|
|
72
|
+
const handleRemoveFromWishlist = async (productId: string) => {
|
|
73
|
+
try {
|
|
74
|
+
await removeFromWishlist(productId);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('Error removing from wishlist:', err);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleClearWishlist = async () => {
|
|
81
|
+
const confirmed = window.confirm('Clear all items from your wishlist?');
|
|
82
|
+
if (!confirmed) return;
|
|
83
|
+
try {
|
|
84
|
+
await clearWishlist();
|
|
85
|
+
await refreshWishlist();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error('Error clearing wishlist:', err);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const totalValue = useMemo(
|
|
92
|
+
() => wishlistProducts.reduce((sum, product) => sum + (product.finalPrice ?? 0), 0),
|
|
93
|
+
[wishlistProducts]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const totalSavings = useMemo(
|
|
97
|
+
() =>
|
|
98
|
+
wishlistProducts.reduce((sum, product) => {
|
|
99
|
+
const before = product.priceBeforeDiscount ?? product.finalPrice ?? 0;
|
|
100
|
+
const after = product.finalPrice ?? 0;
|
|
101
|
+
const savings = Math.max(before - after, 0);
|
|
102
|
+
return sum + savings;
|
|
103
|
+
}, 0),
|
|
104
|
+
[wishlistProducts]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const inStockCount = useMemo(
|
|
108
|
+
() => wishlistProducts.filter((product) => (product.inventoryCount ?? 0) > 0).length,
|
|
109
|
+
[wishlistProducts]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const processedProducts = useMemo(() => {
|
|
113
|
+
let list = [...wishlistProducts];
|
|
114
|
+
|
|
115
|
+
if (onlyInStock) {
|
|
116
|
+
list = list.filter((product) => (product.inventoryCount ?? 0) > 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
switch (sortOption) {
|
|
120
|
+
case 'price-low':
|
|
121
|
+
list.sort((a, b) => (a.finalPrice ?? 0) - (b.finalPrice ?? 0));
|
|
122
|
+
break;
|
|
123
|
+
case 'price-high':
|
|
124
|
+
list.sort((a, b) => (b.finalPrice ?? 0) - (a.finalPrice ?? 0));
|
|
125
|
+
break;
|
|
126
|
+
case 'name':
|
|
127
|
+
list.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
128
|
+
break;
|
|
129
|
+
case 'availability':
|
|
130
|
+
list.sort((a, b) => (b.inventoryCount ?? 0) - (a.inventoryCount ?? 0));
|
|
131
|
+
break;
|
|
132
|
+
case 'featured':
|
|
133
|
+
default:
|
|
134
|
+
list.sort((a, b) => (b.totalSold ?? 0) - (a.totalSold ?? 0));
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return list;
|
|
139
|
+
}, [wishlistProducts, onlyInStock, sortOption]);
|
|
140
|
+
|
|
141
|
+
const emptyAfterFiltering = !isLoading && wishlistProducts.length > 0 && processedProducts.length === 0;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="min-h-screen bg-slate-50 pb-16">
|
|
145
|
+
<section className="relative overflow-hidden bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] pb-32 pt-16 text-white">
|
|
146
|
+
<div className="absolute inset-0 opacity-40 mix-blend-soft-light" aria-hidden="true">
|
|
147
|
+
<div className="absolute -top-1/2 right-1/2 h-[40rem] w-[40rem] rounded-full bg-white/10 blur-3xl" />
|
|
148
|
+
<div className="absolute left-1/4 top-1/4 h-48 w-48 rounded-full bg-white/20 blur-2xl" />
|
|
149
|
+
</div>
|
|
150
|
+
<div className="relative container mx-auto px-4">
|
|
151
|
+
<div className="max-w-3xl space-y-6">
|
|
152
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-4 py-1 text-sm font-semibold uppercase tracking-[0.35em] text-white/70 backdrop-blur">
|
|
153
|
+
<Heart className="h-4 w-4" />
|
|
154
|
+
Wishlist
|
|
155
|
+
</span>
|
|
156
|
+
<h1 className="text-4xl font-bold leading-tight md:text-5xl">
|
|
157
|
+
Curate your pharmacy must-haves in one calming space
|
|
158
|
+
</h1>
|
|
159
|
+
<p className="max-w-2xl text-white/80 md:text-lg">
|
|
160
|
+
We keep your favourite products ready for reorder, refill reminders, and quick checkout—exactly when you need them.
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<div className="relative -mt-20">
|
|
167
|
+
<div className="container mx-auto px-4">
|
|
168
|
+
<motion.div
|
|
169
|
+
initial={{ opacity: 0, y: 24 }}
|
|
170
|
+
animate={{ opacity: 1, y: 0 }}
|
|
171
|
+
className="rounded-3xl border border-slate-100 bg-white p-6 shadow-xl shadow-primary-50"
|
|
172
|
+
>
|
|
173
|
+
<div className="flex flex-col gap-6">
|
|
174
|
+
{!isAuthenticated && (
|
|
175
|
+
<div className="flex min-h-[40vh] items-center justify-center">
|
|
176
|
+
<div className="max-w-lg rounded-3xl border border-slate-100 bg-white p-10 text-center shadow-lg shadow-primary-50">
|
|
177
|
+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-primary-50 text-primary-600">
|
|
178
|
+
<Heart className="h-8 w-8" />
|
|
179
|
+
</div>
|
|
180
|
+
<h2 className="mt-6 text-3xl font-bold text-slate-900">Sign in to see your favourites</h2>
|
|
181
|
+
<p className="mt-3 text-slate-500">
|
|
182
|
+
Create your curated shelf of products and we'll keep them ready whenever you return.
|
|
183
|
+
</p>
|
|
184
|
+
<Button className="mt-6" onClick={() => router.push('/login')}>
|
|
185
|
+
Sign In
|
|
186
|
+
</Button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{isAuthenticated && (
|
|
192
|
+
<>
|
|
193
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
194
|
+
<div className="space-y-1">
|
|
195
|
+
<h2 className="text-2xl font-semibold text-slate-900">Your saved collection</h2>
|
|
196
|
+
<p className="text-sm text-slate-500">
|
|
197
|
+
Total value: <span className="font-semibold text-primary-600">{formatPrice(totalValue)}</span>
|
|
198
|
+
{onlyInStock && ' • Showing items ready to ship'}
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
203
|
+
<label className="inline-flex cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm font-medium text-slate-600 transition hover:border-primary-200 hover:text-primary-600">
|
|
204
|
+
<input
|
|
205
|
+
type="checkbox"
|
|
206
|
+
checked={onlyInStock}
|
|
207
|
+
onChange={(event) => setOnlyInStock(event.target.checked)}
|
|
208
|
+
className="h-4 w-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
|
|
209
|
+
/>
|
|
210
|
+
Only show in-stock
|
|
211
|
+
</label>
|
|
212
|
+
|
|
213
|
+
<div className="flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm text-slate-600">
|
|
214
|
+
<span>Sort</span>
|
|
215
|
+
<select
|
|
216
|
+
value={sortOption}
|
|
217
|
+
onChange={(event) => setSortOption(event.target.value as SortOption)}
|
|
218
|
+
className="bg-transparent text-sm font-medium text-slate-700 outline-none"
|
|
219
|
+
>
|
|
220
|
+
{SORT_OPTIONS.map((option) => (
|
|
221
|
+
<option key={option.value} value={option.value}>
|
|
222
|
+
{option.label}
|
|
223
|
+
</option>
|
|
224
|
+
))}
|
|
225
|
+
</select>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="flex overflow-hidden rounded-full border border-slate-200 bg-slate-50">
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={() => setViewMode('grid')}
|
|
232
|
+
className={`flex items-center gap-1 px-3 py-1.5 text-sm font-medium transition ${viewMode === 'grid'
|
|
233
|
+
? 'bg-primary-600 text-white shadow-lg shadow-primary-500/30'
|
|
234
|
+
: 'text-slate-600 hover:bg-white'
|
|
235
|
+
}`}
|
|
236
|
+
>
|
|
237
|
+
<Grid className="h-4 w-4" />
|
|
238
|
+
Grid
|
|
239
|
+
</button>
|
|
240
|
+
<button
|
|
241
|
+
type="button"
|
|
242
|
+
onClick={() => setViewMode('list')}
|
|
243
|
+
className={`flex items-center gap-1 px-3 py-1.5 text-sm font-medium transition ${viewMode === 'list'
|
|
244
|
+
? 'bg-primary-600 text-white shadow-lg shadow-primary-500/30'
|
|
245
|
+
: 'text-slate-600 hover:bg-white'
|
|
246
|
+
}`}
|
|
247
|
+
>
|
|
248
|
+
<List className="h-4 w-4" />
|
|
249
|
+
List
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{wishlistCount > 0 && (
|
|
254
|
+
<Button
|
|
255
|
+
variant="ghost"
|
|
256
|
+
className="text-sm font-semibold text-slate-500 hover:text-red-500"
|
|
257
|
+
onClick={handleClearWishlist}
|
|
258
|
+
>
|
|
259
|
+
<Trash2 className="h-4 w-4" />
|
|
260
|
+
Clear all
|
|
261
|
+
</Button>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{isLoading && (
|
|
267
|
+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
268
|
+
{Array.from({ length: Math.min(wishlistCount || 3, 6) }).map((_, index) => (
|
|
269
|
+
<div
|
|
270
|
+
key={index}
|
|
271
|
+
className="h-72 animate-pulse rounded-2xl border border-slate-200 bg-slate-100"
|
|
272
|
+
/>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{!isLoading && wishlistCount === 0 && (
|
|
278
|
+
<div className="flex min-h-[30vh] items-center justify-center">
|
|
279
|
+
<div className="max-w-2xl rounded-3xl border border-slate-100 bg-white p-12 text-center shadow-xl shadow-primary-50">
|
|
280
|
+
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-primary-100 text-primary-600">
|
|
281
|
+
<Sparkles className="h-10 w-10" />
|
|
282
|
+
</div>
|
|
283
|
+
<h2 className="mt-6 text-4xl font-bold text-slate-900">Start your wellness wishlist</h2>
|
|
284
|
+
<p className="mt-4 text-lg text-slate-500">
|
|
285
|
+
Bookmark pharmacy essentials, supplements, or skincare picks and we'll keep them safe until you're ready to checkout.
|
|
286
|
+
</p>
|
|
287
|
+
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
|
288
|
+
<Button onClick={() => router.push('/shop')}>Discover products</Button>
|
|
289
|
+
<Button variant="outline" onClick={() => router.push('/categories')}>
|
|
290
|
+
Browse categories
|
|
291
|
+
</Button>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
{!isLoading && processedProducts.length > 0 && (
|
|
299
|
+
<>
|
|
300
|
+
{viewMode === 'grid' ? (
|
|
301
|
+
<motion.div
|
|
302
|
+
layout
|
|
303
|
+
className="grid grid-cols-1 gap-5 sm:grid-cols-2 xl:grid-cols-3"
|
|
304
|
+
>
|
|
305
|
+
<AnimatePresence>
|
|
306
|
+
{processedProducts.map((product) => (
|
|
307
|
+
<motion.div
|
|
308
|
+
key={product.id}
|
|
309
|
+
layout
|
|
310
|
+
initial={{ opacity: 0, y: 20 }}
|
|
311
|
+
animate={{ opacity: 1, y: 0 }}
|
|
312
|
+
exit={{ opacity: 0, y: -20 }}
|
|
313
|
+
transition={{ duration: 0.2 }}
|
|
314
|
+
>
|
|
315
|
+
<ProductCard
|
|
316
|
+
product={product as ExtendedProductDTO}
|
|
317
|
+
onClickProduct={(p) => router.push(`/products/${p.id}`)}
|
|
318
|
+
onFavorite={() => handleRemoveFromWishlist(product.id)}
|
|
319
|
+
isFavorited
|
|
320
|
+
/>
|
|
321
|
+
</motion.div>
|
|
322
|
+
))}
|
|
323
|
+
</AnimatePresence>
|
|
324
|
+
</motion.div>
|
|
325
|
+
) : (
|
|
326
|
+
<motion.div layout className="space-y-4">
|
|
327
|
+
<AnimatePresence>
|
|
328
|
+
{processedProducts.map((product) => (
|
|
329
|
+
<motion.div
|
|
330
|
+
key={product.id}
|
|
331
|
+
layout
|
|
332
|
+
initial={{ opacity: 0, y: 20 }}
|
|
333
|
+
animate={{ opacity: 1, y: 0 }}
|
|
334
|
+
exit={{ opacity: 0, y: -20 }}
|
|
335
|
+
transition={{ duration: 0.2 }}
|
|
336
|
+
className="flex flex-col gap-4 rounded-2xl border border-slate-100 bg-slate-50 p-4 shadow-sm shadow-primary-50 sm:flex-row sm:items-center"
|
|
337
|
+
>
|
|
338
|
+
<div className="relative h-28 w-full overflow-hidden rounded-2xl bg-white sm:w-40">
|
|
339
|
+
<Image
|
|
340
|
+
fill
|
|
341
|
+
src={product.productMedia?.[0]?.file || '/placeholder-product.jpg'}
|
|
342
|
+
alt={product.name || 'Wishlist item'}
|
|
343
|
+
className="h-full w-full object-cover"
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="flex flex-1 flex-col gap-2">
|
|
347
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
348
|
+
<div>
|
|
349
|
+
<h3 className="text-lg font-semibold text-slate-900">
|
|
350
|
+
{product.name}
|
|
351
|
+
</h3>
|
|
352
|
+
<p className="text-sm text-slate-500">
|
|
353
|
+
{product.parentCategories?.map((category) => category?.name).join(', ') || 'General wellness'}
|
|
354
|
+
</p>
|
|
355
|
+
</div>
|
|
356
|
+
<div className="text-right">
|
|
357
|
+
<p className="text-lg font-bold text-primary-600">
|
|
358
|
+
{formatPrice(product.finalPrice ?? 0)}
|
|
359
|
+
</p>
|
|
360
|
+
{product.isDiscounted && (
|
|
361
|
+
<p className="text-xs text-emerald-500">
|
|
362
|
+
You save {formatPrice(Math.max((product.priceBeforeDiscount ?? 0) - (product.finalPrice ?? 0), 0))}
|
|
363
|
+
</p>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-500">
|
|
368
|
+
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 font-medium ${product.inventoryCount > 0 ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'}`}>
|
|
369
|
+
<Package className="h-3.5 w-3.5" />
|
|
370
|
+
{product.inventoryCount > 0 ? 'In stock' : 'Backordered'}
|
|
371
|
+
</span>
|
|
372
|
+
{product.totalSold > 0 && (
|
|
373
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-slate-200 px-2.5 py-1 font-medium text-slate-700">
|
|
374
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
375
|
+
{product.totalSold}+ purchased
|
|
376
|
+
</span>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
<div className="flex flex-wrap gap-2">
|
|
380
|
+
<Button
|
|
381
|
+
size="sm"
|
|
382
|
+
onClick={() => router.push(`/products/${product.id}`)}
|
|
383
|
+
>
|
|
384
|
+
View details
|
|
385
|
+
</Button>
|
|
386
|
+
<Button
|
|
387
|
+
size="sm"
|
|
388
|
+
variant="outline"
|
|
389
|
+
onClick={() => handleRemoveFromWishlist(product.id)}
|
|
390
|
+
className="text-primary-600"
|
|
391
|
+
>
|
|
392
|
+
Remove
|
|
393
|
+
</Button>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
</motion.div>
|
|
397
|
+
))}
|
|
398
|
+
</AnimatePresence>
|
|
399
|
+
</motion.div>
|
|
400
|
+
)}
|
|
401
|
+
</>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{isAuthenticated && emptyAfterFiltering && (
|
|
405
|
+
<div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200 bg-slate-50 p-12 text-center">
|
|
406
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-slate-200 text-slate-500">
|
|
407
|
+
<Package className="h-8 w-8" />
|
|
408
|
+
</div>
|
|
409
|
+
<h3 className="mt-6 text-2xl font-semibold text-slate-900">
|
|
410
|
+
Nothing matches those filters
|
|
411
|
+
</h3>
|
|
412
|
+
<p className="mt-2 max-w-md text-sm text-slate-500">
|
|
413
|
+
Try showing out-of-stock items or adjust your sort order to revisit everything you’ve saved.
|
|
414
|
+
</p>
|
|
415
|
+
<Button className="mt-6" variant="outline" onClick={() => setOnlyInStock(false)}>
|
|
416
|
+
Show all saved products
|
|
417
|
+
</Button>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
</>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
</motion.div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/* tslint:disable */
|
|
2
|
-
/* eslint-disable */
|
|
3
|
-
/**
|
|
4
|
-
* Hey Pharamcist API
|
|
5
|
-
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
|
|
6
|
-
*
|
|
7
|
-
* OpenAPI spec version: 1.0
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* NOTE: This class is auto generated by the swagger code generator program.
|
|
11
|
-
* https://github.com/swagger-api/swagger-codegen.git
|
|
12
|
-
* Do not edit the class manually.
|
|
13
|
-
*/
|
|
14
|
-
import { ProductVariant } from './product-variant';
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
* @export
|
|
18
|
-
* @interface InventoryPaginatedResponse
|
|
19
|
-
*/
|
|
20
|
-
export interface InventoryPaginatedResponse {
|
|
21
|
-
_id?: string;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
* @type {string}
|
|
25
|
-
* @memberof InventoryPaginatedResponse
|
|
26
|
-
*/
|
|
27
|
-
storeId?: string;
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
* @type {ProductVariant}
|
|
31
|
-
* @memberof InventoryPaginatedResponse
|
|
32
|
-
*/
|
|
33
|
-
variant?: ProductVariant;
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
* @type {number}
|
|
37
|
-
* @memberof InventoryPaginatedResponse
|
|
38
|
-
*/
|
|
39
|
-
quantity?: number;
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
* @type {string}
|
|
43
|
-
* @memberof InventoryPaginatedResponse
|
|
44
|
-
*/
|
|
45
|
-
status?: InventoryPaginatedResponseStatusEnum;
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
* @type {number}
|
|
49
|
-
* @memberof InventoryPaginatedResponse
|
|
50
|
-
*/
|
|
51
|
-
total: number;
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
* @type {number}
|
|
55
|
-
* @memberof InventoryPaginatedResponse
|
|
56
|
-
*/
|
|
57
|
-
page: number;
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
* @type {number}
|
|
61
|
-
* @memberof InventoryPaginatedResponse
|
|
62
|
-
*/
|
|
63
|
-
limit: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* @export
|
|
68
|
-
* @enum {string}
|
|
69
|
-
*/
|
|
70
|
-
export enum InventoryPaginatedResponseStatusEnum {
|
|
71
|
-
INSTOCK = 'IN_STOCK',
|
|
72
|
-
OUTOFSTOCK = 'OUT_OF_STOCK',
|
|
73
|
-
LOWSTOCK = 'LOW_STOCK'
|
|
74
|
-
}
|
|
75
|
-
|
package/src/lib/api/auth.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { authAdapter } from '@/lib/api-adapter';
|
|
2
|
-
import { User, ApiResponse, LoginCredentials, RegisterData, AuthTokens } from '@/lib/types';
|
|
3
|
-
|
|
4
|
-
export const authApi = {
|
|
5
|
-
/**
|
|
6
|
-
* Register a new user
|
|
7
|
-
*/
|
|
8
|
-
async register(data: RegisterData): Promise<ApiResponse<{ user: User; tokens: AuthTokens }>> {
|
|
9
|
-
const response = await authAdapter.register(data);
|
|
10
|
-
return {
|
|
11
|
-
...response,
|
|
12
|
-
data: {
|
|
13
|
-
user: response.data.user,
|
|
14
|
-
tokens: {
|
|
15
|
-
accessToken: response.data.token,
|
|
16
|
-
refreshToken: response.data.token, // Backend uses single token
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Login user
|
|
24
|
-
*/
|
|
25
|
-
async login(credentials: LoginCredentials): Promise<ApiResponse<{ user: User; tokens: AuthTokens }>> {
|
|
26
|
-
const response = await authAdapter.login(credentials);
|
|
27
|
-
return {
|
|
28
|
-
...response,
|
|
29
|
-
data: {
|
|
30
|
-
user: response.data.user,
|
|
31
|
-
tokens: {
|
|
32
|
-
accessToken: response.data.token,
|
|
33
|
-
refreshToken: response.data.token, // Backend uses single token
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Logout user
|
|
41
|
-
*/
|
|
42
|
-
async logout(): Promise<void> {
|
|
43
|
-
await authAdapter.logout();
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Get current user
|
|
48
|
-
*/
|
|
49
|
-
async getCurrentUser(): Promise<ApiResponse<User>> {
|
|
50
|
-
return await authAdapter.getCurrentUser();
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Update user profile
|
|
55
|
-
*/
|
|
56
|
-
async updateProfile(data: Partial<User>): Promise<ApiResponse<User>> {
|
|
57
|
-
return await authAdapter.updateProfile(data);
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Change password
|
|
62
|
-
*/
|
|
63
|
-
async changePassword(currentPassword: string, newPassword: string): Promise<ApiResponse<void>> {
|
|
64
|
-
return await authAdapter.changePassword(currentPassword, newPassword);
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Request password reset
|
|
69
|
-
*/
|
|
70
|
-
async forgotPassword(email: string): Promise<ApiResponse<void>> {
|
|
71
|
-
return await authAdapter.forgotPassword(email);
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Reset password with token
|
|
76
|
-
*/
|
|
77
|
-
async resetPassword(token: string, newPassword: string): Promise<ApiResponse<void>> {
|
|
78
|
-
return await authAdapter.resetPassword(token, newPassword);
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
package/src/lib/api/cart.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { cartAdapter } from '@/lib/api-adapter';
|
|
2
|
-
import { Cart, ApiResponse } from '@/lib/types';
|
|
3
|
-
|
|
4
|
-
export const cartApi = {
|
|
5
|
-
/**
|
|
6
|
-
* Get user's cart
|
|
7
|
-
*/
|
|
8
|
-
async getCart(): Promise<ApiResponse<Cart>> {
|
|
9
|
-
return await cartAdapter.getCart();
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Add item to cart
|
|
14
|
-
* Note: Backend uses product variants. If variant ID not provided, uses product ID as variant
|
|
15
|
-
*/
|
|
16
|
-
async addToCart(productId: string, quantity: number = 1): Promise<ApiResponse<Cart>> {
|
|
17
|
-
// Use product ID as variant ID for simplified products
|
|
18
|
-
return await cartAdapter.addToCart(productId, productId, quantity);
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Update cart item quantity
|
|
23
|
-
*/
|
|
24
|
-
async updateCartItem(productId: string, quantity: number): Promise<ApiResponse<Cart>> {
|
|
25
|
-
return await cartAdapter.updateCartItem(productId, productId, quantity);
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Remove item from cart
|
|
30
|
-
*/
|
|
31
|
-
async removeFromCart(productId: string): Promise<ApiResponse<Cart>> {
|
|
32
|
-
return await cartAdapter.removeFromCart(productId, productId);
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Clear cart
|
|
37
|
-
*/
|
|
38
|
-
async clearCart(): Promise<ApiResponse<void>> {
|
|
39
|
-
return await cartAdapter.clearCart();
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|