hey-pharmacist-ecommerce 1.0.5 → 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 -3866
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6756 -3818
- 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 -152
- package/src/lib/api-adapter/orders-adapter.ts +0 -195
- package/src/lib/api-adapter/products-adapter.ts +0 -194
|
@@ -2,37 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { motion } from 'framer-motion';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ArrowRight,
|
|
7
|
+
BadgePercent,
|
|
8
|
+
HeartPulse,
|
|
9
|
+
ShieldCheck,
|
|
10
|
+
ShoppingBag,
|
|
11
|
+
} from 'lucide-react';
|
|
6
12
|
import { CartItem } from '@/components/CartItem';
|
|
7
13
|
import { EmptyState } from '@/components/EmptyState';
|
|
8
14
|
import { Button } from '@/components/ui/Button';
|
|
9
15
|
import { useCart } from '@/providers/CartProvider';
|
|
10
16
|
import { formatPrice } from '@/lib/utils/format';
|
|
11
17
|
import { useRouter } from 'next/navigation';
|
|
18
|
+
import { CartItemPopulated } from '@/lib/Apis';
|
|
19
|
+
|
|
12
20
|
|
|
13
21
|
export function CartScreen() {
|
|
14
22
|
const router = useRouter();
|
|
15
23
|
const { cart, isLoading } = useCart();
|
|
16
24
|
|
|
17
|
-
if (
|
|
18
|
-
return (
|
|
19
|
-
<div className="min-h-screen bg-gray-50 py-12">
|
|
20
|
-
<div className="container mx-auto px-4">
|
|
21
|
-
<p>Loading cart...</p>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!cart || cart.items.length === 0) {
|
|
25
|
+
if (!cart || cart.cartBody.items.length === 0) {
|
|
28
26
|
return (
|
|
29
|
-
<div className="min-h-screen bg-
|
|
30
|
-
<div className="
|
|
27
|
+
<div className="min-h-screen bg-gradient-to-br from-primary-700 via-primary-600 to-secondary-600 flex items-center justify-center">
|
|
28
|
+
<div className="mx-auto px-20 py-5 bg-white rounded-3xl">
|
|
31
29
|
<EmptyState
|
|
32
30
|
icon={ShoppingBag}
|
|
33
|
-
title="Your
|
|
34
|
-
description="Add
|
|
35
|
-
actionLabel="
|
|
31
|
+
title="Your bag feels a little empty"
|
|
32
|
+
description="Add pharmacy favorites to unlock quick shipping, curated bundles, and personalized recommendations."
|
|
33
|
+
actionLabel="Discover products"
|
|
36
34
|
onAction={() => router.push('/shop')}
|
|
37
35
|
/>
|
|
38
36
|
</div>
|
|
@@ -41,100 +39,138 @@ export function CartScreen() {
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
const subtotal = cart.total;
|
|
44
|
-
const shipping =
|
|
45
|
-
const tax =
|
|
42
|
+
const shipping = 0;
|
|
43
|
+
const tax = 0;
|
|
46
44
|
const total = subtotal + shipping + tax;
|
|
45
|
+
|
|
47
46
|
|
|
48
47
|
return (
|
|
49
|
-
<div className="min-h-screen bg-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
<div className="min-h-screen bg-slate-50">
|
|
49
|
+
<section className="relative overflow-hidden bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] text-white">
|
|
50
|
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]" />
|
|
51
|
+
<div className="relative container mx-auto px-4 py-16 mb-8">
|
|
52
|
+
<motion.div
|
|
53
|
+
initial={{ opacity: 0, y: 24 }}
|
|
54
|
+
animate={{ opacity: 1, y: 0 }}
|
|
55
|
+
className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between"
|
|
56
|
+
>
|
|
57
|
+
<div className="max-w-2xl space-y-4">
|
|
58
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-semibold tracking-wide text-white/80 backdrop-blur">
|
|
59
|
+
<HeartPulse className="h-4 w-4" />
|
|
60
|
+
Wellness essentials, ready when you are
|
|
61
|
+
</span>
|
|
62
|
+
<h1 className="text-4xl font-bold md:text-5xl">Your curated cart</h1>
|
|
63
|
+
<p className="text-white/75 md:text-lg">
|
|
64
|
+
Review your selections, unlock exclusive perks, and check out with pharmacist-backed
|
|
65
|
+
confidence.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="rounded-3xl bg-white/15 p-6 backdrop-blur-md">
|
|
69
|
+
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-white/70">
|
|
70
|
+
Cart summary
|
|
71
|
+
</p>
|
|
72
|
+
<p className="mt-4 text-4xl font-semibold">{formatPrice(total)}</p>
|
|
73
|
+
<p className="text-sm text-white/70">Taxes and shipping calculated below</p>
|
|
74
|
+
</div>
|
|
75
|
+
</motion.div>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
<div className="relative -mt-16 pb-20">
|
|
80
|
+
<div className="container mx-auto px-4">
|
|
81
|
+
<div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
|
82
|
+
<motion.section
|
|
83
|
+
initial={{ opacity: 0, y: 24 }}
|
|
84
|
+
animate={{ opacity: 1, y: 0 }}
|
|
85
|
+
transition={{ delay: 0.05 }}
|
|
86
|
+
className="space-y-6 rounded-3xl border border-slate-100 bg-white p-6 shadow-lg shadow-primary-50"
|
|
87
|
+
>
|
|
88
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
89
|
+
<h2 className="text-xl font-semibold text-slate-900">
|
|
90
|
+
Items ({cart.cartBody.items.length})
|
|
91
|
+
</h2>
|
|
92
|
+
<div className="inline-flex items-center gap-2 rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700">
|
|
93
|
+
<ShieldCheck className="h-4 w-4" />
|
|
94
|
+
Guaranteed cold-chain handling
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
{isLoading && (
|
|
98
|
+
<div className="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600">
|
|
99
|
+
<span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600" />
|
|
100
|
+
Updating cart…
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
<div className="space-y-5">
|
|
104
|
+
{cart.cartBody.items.map((item: CartItemPopulated) => (
|
|
105
|
+
<CartItem key={item.productVariantId} item={item} />
|
|
67
106
|
))}
|
|
68
107
|
</div>
|
|
69
|
-
</
|
|
70
|
-
|
|
71
|
-
{/* Order Summary */}
|
|
72
|
-
<div className="lg:col-span-1">
|
|
73
|
-
<motion.div
|
|
74
|
-
initial={{ opacity: 0, y: 20 }}
|
|
75
|
-
animate={{ opacity: 1, y: 0 }}
|
|
76
|
-
transition={{ delay: 0.1 }}
|
|
77
|
-
className="bg-white rounded-2xl p-6 shadow-sm sticky top-24"
|
|
78
|
-
>
|
|
79
|
-
<h2 className="text-2xl font-bold text-gray-900 mb-6">Order Summary</h2>
|
|
108
|
+
</motion.section>
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
|
|
110
|
+
<motion.aside
|
|
111
|
+
initial={{ opacity: 0, y: 24 }}
|
|
112
|
+
animate={{ opacity: 1, y: 0 }}
|
|
113
|
+
transition={{ delay: 0.1 }}
|
|
114
|
+
className="space-y-6 lg:sticky lg:top-28"
|
|
115
|
+
>
|
|
116
|
+
<div className="rounded-3xl border border-slate-100 bg-white p-6 shadow-lg shadow-primary-50">
|
|
117
|
+
<div className="flex items-center justify-between">
|
|
118
|
+
<h2 className="text-xl font-semibold text-slate-900">Checkout summary</h2>
|
|
119
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold text-primary-700">
|
|
120
|
+
<BadgePercent className="h-4 w-4" />
|
|
121
|
+
Savings applied
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div className="mt-6 space-y-4 text-sm text-slate-600">
|
|
125
|
+
<div className="flex items-center justify-between">
|
|
83
126
|
<span>Subtotal</span>
|
|
84
|
-
<span className="font-
|
|
127
|
+
<span className="font-semibold text-slate-900">{formatPrice(subtotal)}</span>
|
|
85
128
|
</div>
|
|
86
|
-
<div className="flex justify-between
|
|
129
|
+
<div className="flex items-center justify-between">
|
|
87
130
|
<span>Shipping</span>
|
|
88
|
-
<span className="font-
|
|
89
|
-
|
|
90
|
-
<span className="text-green-600">FREE</span>
|
|
91
|
-
) : (
|
|
92
|
-
formatPrice(shipping)
|
|
93
|
-
)}
|
|
131
|
+
<span className="font-semibold">
|
|
132
|
+
Will be calculated at checkout
|
|
94
133
|
</span>
|
|
95
134
|
</div>
|
|
96
|
-
<div className="
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<div className="border-t border-gray-200 pt-4">
|
|
101
|
-
<div className="flex justify-between">
|
|
102
|
-
<span className="text-xl font-bold text-gray-900">Total</span>
|
|
103
|
-
<span className="text-2xl font-bold text-gray-900">
|
|
104
|
-
{formatPrice(total)}
|
|
105
|
-
</span>
|
|
135
|
+
<div className="rounded-2xl bg-slate-50 p-4">
|
|
136
|
+
<div className="flex items-center justify-between text-base font-semibold text-slate-900">
|
|
137
|
+
<span>Order total</span>
|
|
138
|
+
<span>{formatPrice(total)}</span>
|
|
106
139
|
</div>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
{shipping > 0 && (
|
|
111
|
-
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
112
|
-
<p className="text-sm text-blue-800">
|
|
113
|
-
Add {formatPrice(50 - subtotal)} more to get FREE shipping!
|
|
140
|
+
<p className="mt-2 text-xs text-slate-500">
|
|
141
|
+
Prices include pharmacy-grade quality control and packaging.
|
|
114
142
|
</p>
|
|
115
143
|
</div>
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
</div>
|
|
118
145
|
<Button
|
|
119
146
|
size="lg"
|
|
147
|
+
className="mt-6 w-full"
|
|
120
148
|
onClick={() => router.push('/checkout')}
|
|
121
|
-
className="w-full"
|
|
122
149
|
>
|
|
123
|
-
|
|
124
|
-
<ArrowRight className="
|
|
150
|
+
Secure checkout
|
|
151
|
+
<ArrowRight className="h-5 w-5" />
|
|
125
152
|
</Button>
|
|
126
|
-
|
|
127
153
|
<button
|
|
154
|
+
type="button"
|
|
128
155
|
onClick={() => router.push('/shop')}
|
|
129
|
-
className="w-full
|
|
156
|
+
className="mt-4 w-full text-sm font-semibold text-primary-600 transition hover:text-primary-700"
|
|
130
157
|
>
|
|
131
|
-
Continue
|
|
158
|
+
Continue shopping
|
|
132
159
|
</button>
|
|
133
|
-
</
|
|
134
|
-
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div className="rounded-3xl border border-primary-100 bg-primary-50/70 p-6 text-sm text-primary-700 shadow-sm">
|
|
163
|
+
<p className="font-semibold uppercase tracking-[0.3em]">Need help?</p>
|
|
164
|
+
<p className="mt-2 leading-relaxed">
|
|
165
|
+
Chat with a pharmacist to optimize your regimen or discuss substitutions before you
|
|
166
|
+
check out.
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
</motion.aside>
|
|
135
170
|
</div>
|
|
136
171
|
</div>
|
|
137
172
|
</div>
|
|
173
|
+
</div>
|
|
138
174
|
);
|
|
139
175
|
}
|
|
140
176
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import { Package, Sparkles } from 'lucide-react';
|
|
6
|
+
import { EmptyState } from '@/components/EmptyState';
|
|
7
|
+
import { useCategories } from '@/hooks/useProducts';
|
|
8
|
+
import Image from 'next/image';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { useRouter } from 'next/navigation';
|
|
11
|
+
|
|
12
|
+
export function CategoriesScreen() {
|
|
13
|
+
const { categories, isLoading } = useCategories();
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen bg-slate-50">
|
|
18
|
+
<section className="relative overflow-hidden bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] text-white mb-8">
|
|
19
|
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]" />
|
|
20
|
+
<div className="relative container mx-auto px-4 py-16">
|
|
21
|
+
<motion.div
|
|
22
|
+
initial={{ opacity: 0, y: 24 }}
|
|
23
|
+
animate={{ opacity: 1, y: 0 }}
|
|
24
|
+
className="space-y-6"
|
|
25
|
+
>
|
|
26
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-semibold uppercase tracking-[0.35em] text-white/70 backdrop-blur">
|
|
27
|
+
<Package className="h-4 w-4" />
|
|
28
|
+
Product Categories
|
|
29
|
+
</span>
|
|
30
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
31
|
+
<div className="space-y-4">
|
|
32
|
+
<h1 className="text-4xl font-bold md:text-5xl">Browse Our Product Range</h1>
|
|
33
|
+
<p className="max-w-2xl text-white/80 md:text-lg">
|
|
34
|
+
Explore our comprehensive selection of healthcare products, carefully curated by our pharmacists to meet all your wellness needs.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="rounded-3xl bg-white/15 p-6 backdrop-blur">
|
|
38
|
+
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-white/70">
|
|
39
|
+
Quick tip
|
|
40
|
+
</p>
|
|
41
|
+
<p className="mt-3 text-sm text-white/80">
|
|
42
|
+
Use the categories below to quickly find the products you're looking for, or use the search function for specific items.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</motion.div>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<div className="relative -mt-16 pb-16">
|
|
51
|
+
<div className="container mx-auto px-4">
|
|
52
|
+
<motion.div
|
|
53
|
+
initial={{ opacity: 0, y: 24 }}
|
|
54
|
+
animate={{ opacity: 1, y: 0 }}
|
|
55
|
+
className="rounded-3xl border border-slate-100 bg-white p-6 shadow-lg shadow-primary-50"
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center gap-3 text-sm text-slate-500 mb-6">
|
|
58
|
+
<Sparkles className="h-4 w-4 text-primary-500" />
|
|
59
|
+
<span>Browse our complete product catalog organized by categories.</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{isLoading ? (
|
|
63
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
64
|
+
{[...Array(8)].map((_, i) => (
|
|
65
|
+
<div key={i} className="animate-pulse">
|
|
66
|
+
<div className="bg-gray-200 rounded-lg aspect-square mb-2"></div>
|
|
67
|
+
<div className="h-4 bg-gray-200 rounded w-3/4 mb-1"></div>
|
|
68
|
+
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
) : categories.length > 0 ? (
|
|
73
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
74
|
+
{categories.map((category) => (
|
|
75
|
+
<Link
|
|
76
|
+
key={category.id}
|
|
77
|
+
href={`/shop?category=${category.name}`}
|
|
78
|
+
className="group block overflow-hidden rounded-xl border border-gray-100 bg-white p-4 text-center transition hover:shadow-lg hover:border-primary-500"
|
|
79
|
+
>
|
|
80
|
+
<div className="relative aspect-square w-full overflow-hidden rounded-lg bg-gray-50 mb-3">
|
|
81
|
+
{category.image ? (
|
|
82
|
+
<Image
|
|
83
|
+
src={category.image}
|
|
84
|
+
alt={category.name || 'Category Image'}
|
|
85
|
+
fill
|
|
86
|
+
className="object-cover transition-transform group-hover:scale-105"
|
|
87
|
+
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 25vw"
|
|
88
|
+
/>
|
|
89
|
+
) : (
|
|
90
|
+
<div className="flex h-full w-full items-center justify-center bg-gray-100 text-gray-400">
|
|
91
|
+
<Package className="h-12 w-12" />
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
<h3 className="text-lg font-semibold text-gray-900 group-hover:text-primary-600 transition-colors">
|
|
96
|
+
{category.name}
|
|
97
|
+
</h3>
|
|
98
|
+
{category.productCount > 0 && (
|
|
99
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
100
|
+
{category.productCount} {category.productCount === 1 ? 'product' : 'products'}
|
|
101
|
+
</p>
|
|
102
|
+
)}
|
|
103
|
+
</Link>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
) : (
|
|
107
|
+
<EmptyState
|
|
108
|
+
title="No categories found"
|
|
109
|
+
description="There are currently no product categories available."
|
|
110
|
+
icon={Package}
|
|
111
|
+
actionLabel="Shop products"
|
|
112
|
+
onAction={() => router.push('/shop')}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</motion.div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|