@rovela-ai/sdk 0.20.0 → 0.21.0
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/admin/api/index.d.ts +2 -0
- package/dist/admin/api/index.d.ts.map +1 -1
- package/dist/admin/api/index.js +2 -0
- package/dist/admin/api/index.js.map +1 -1
- package/dist/admin/api/integrations.d.ts +60 -0
- package/dist/admin/api/integrations.d.ts.map +1 -0
- package/dist/admin/api/integrations.js +337 -0
- package/dist/admin/api/integrations.js.map +1 -0
- package/dist/admin/components/AdminNav.d.ts.map +1 -1
- package/dist/admin/components/AdminNav.js +3 -1
- package/dist/admin/components/AdminNav.js.map +1 -1
- package/dist/admin/components/AdminSelect.d.ts.map +1 -1
- package/dist/admin/components/AdminSelect.js +2 -0
- package/dist/admin/components/AdminSelect.js.map +1 -1
- package/dist/admin/components/DiscountsManager.d.ts.map +1 -1
- package/dist/admin/components/DiscountsManager.js +2 -2
- package/dist/admin/components/DiscountsManager.js.map +1 -1
- package/dist/admin/components/IntegrationsManager.d.ts +5 -0
- package/dist/admin/components/IntegrationsManager.d.ts.map +1 -0
- package/dist/admin/components/IntegrationsManager.js +138 -0
- package/dist/admin/components/IntegrationsManager.js.map +1 -0
- package/dist/admin/components/OrderDetails.d.ts.map +1 -1
- package/dist/admin/components/OrderDetails.js +8 -4
- package/dist/admin/components/OrderDetails.js.map +1 -1
- package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
- package/dist/admin/components/ShippingSettings.js +21 -21
- package/dist/admin/components/ShippingSettings.js.map +1 -1
- package/dist/admin/components/UsersTable.d.ts.map +1 -1
- package/dist/admin/components/UsersTable.js +2 -0
- package/dist/admin/components/UsersTable.js.map +1 -1
- package/dist/admin/components/VariantManager.d.ts.map +1 -1
- package/dist/admin/components/VariantManager.js +8 -2
- package/dist/admin/components/VariantManager.js.map +1 -1
- package/dist/admin/components/index.d.ts +2 -0
- package/dist/admin/components/index.d.ts.map +1 -1
- package/dist/admin/components/index.js +1 -0
- package/dist/admin/components/index.js.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/integrations/brevo-logo.d.ts +11 -0
- package/dist/admin/integrations/brevo-logo.d.ts.map +1 -0
- package/dist/admin/integrations/brevo-logo.js +11 -0
- package/dist/admin/integrations/brevo-logo.js.map +1 -0
- package/dist/admin/integrations/registry.d.ts +116 -0
- package/dist/admin/integrations/registry.d.ts.map +1 -0
- package/dist/admin/integrations/registry.js +389 -0
- package/dist/admin/integrations/registry.js.map +1 -0
- package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
- package/dist/checkout/components/CheckoutFlow.js +0 -1
- package/dist/checkout/components/CheckoutFlow.js.map +1 -1
- package/dist/core/db/queries.d.ts +25 -25
- package/dist/core/db/queries.d.ts.map +1 -1
- package/dist/core/db/queries.js +5 -0
- package/dist/core/db/queries.js.map +1 -1
- package/dist/core/i18n/messages/en.d.ts.map +1 -1
- package/dist/core/i18n/messages/en.js +123 -0
- package/dist/core/i18n/messages/en.js.map +1 -1
- package/dist/core/i18n/messages/fr.d.ts.map +1 -1
- package/dist/core/i18n/messages/fr.js +123 -0
- package/dist/core/i18n/messages/fr.js.map +1 -1
- package/dist/products/api/products.d.ts +3 -1
- package/dist/products/api/products.d.ts.map +1 -1
- package/dist/products/api/products.js +29 -3
- package/dist/products/api/products.js.map +1 -1
- package/dist/products/components/VariantSelector.d.ts.map +1 -1
- package/dist/products/components/VariantSelector.js +4 -1
- package/dist/products/components/VariantSelector.js.map +1 -1
- package/dist/products/hooks/useProducts.d.ts +3 -1
- package/dist/products/hooks/useProducts.d.ts.map +1 -1
- package/dist/products/hooks/useProducts.js +4 -2
- package/dist/products/hooks/useProducts.js.map +1 -1
- package/docs/changelog/epoch-08-integrations.md +125 -0
- package/package.json +1 -1
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +19 -0
- package/templates/store-template/app/admin/(dashboard)/integrations/page.tsx +17 -0
- package/templates/store-template/app/api/admin/integrations/route.ts +9 -0
- package/templates/store-template/app/faq/page.tsx +42 -56
- package/templates/store-template/app/privacy/page.tsx +65 -105
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +6 -3
- package/templates/store-template/app/products/page.tsx +63 -1
- package/templates/store-template/app/terms/page.tsx +70 -198
- package/templates/store-template/package.json +1 -1
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { Metadata } from 'next'
|
|
2
|
-
import { LocaleLink as Link } from '@rovela-ai/sdk/core'
|
|
3
|
-
import {
|
|
2
|
+
import { LocaleLink as Link, getT } from '@rovela-ai/sdk/core'
|
|
3
|
+
import {
|
|
4
|
+
getBlueprint,
|
|
5
|
+
localizedAlternates,
|
|
6
|
+
findSettings,
|
|
7
|
+
getRequestLocale,
|
|
8
|
+
} from '@rovela-ai/sdk/core/server'
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Privacy Policy Page
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* Standard e-commerce privacy policy, personalized with the store name. All
|
|
14
|
+
* copy comes from the i18n catalogs (`privacy.*` keys) so a French-default
|
|
15
|
+
* store serves a French policy — the body was English literals pre-0.21.
|
|
16
|
+
* The #cookies section's copy stays in lockstep with the cookie banner's
|
|
17
|
+
* three categories (see the cookie-consent module).
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
20
|
// =============================================================================
|
|
@@ -14,6 +22,8 @@ import { getBlueprint, localizedAlternates, findSettings } from '@rovela-ai/sdk/
|
|
|
14
22
|
// =============================================================================
|
|
15
23
|
|
|
16
24
|
export async function generateMetadata(): Promise<Metadata> {
|
|
25
|
+
const { locale } = await getRequestLocale()
|
|
26
|
+
const t = getT(locale)
|
|
17
27
|
let storeName = 'Our Store'
|
|
18
28
|
try {
|
|
19
29
|
const blueprint = getBlueprint()
|
|
@@ -23,8 +33,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
return {
|
|
26
|
-
title: '
|
|
27
|
-
description:
|
|
36
|
+
title: t('privacy.title'),
|
|
37
|
+
description: t('privacy.metaDescription', { storeName }),
|
|
28
38
|
alternates: await localizedAlternates('/privacy'),
|
|
29
39
|
}
|
|
30
40
|
}
|
|
@@ -34,6 +44,9 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
34
44
|
// =============================================================================
|
|
35
45
|
|
|
36
46
|
export default async function PrivacyPage() {
|
|
47
|
+
const { locale } = await getRequestLocale()
|
|
48
|
+
const t = getT(locale)
|
|
49
|
+
|
|
37
50
|
// Get store name for display — LIVE settings first (store_settings is what
|
|
38
51
|
// the merchant edits in /admin; blueprint + env are build-time fallbacks
|
|
39
52
|
// that go stale after a rename). Fail-open on DB errors.
|
|
@@ -77,7 +90,7 @@ export default async function PrivacyPage() {
|
|
|
77
90
|
<div className="max-w-3xl mx-auto">
|
|
78
91
|
{/* Header */}
|
|
79
92
|
<div className="mb-12">
|
|
80
|
-
<h1 className="text-3xl font-bold font-heading">
|
|
93
|
+
<h1 className="text-3xl font-bold font-heading">{t('privacy.title')}</h1>
|
|
81
94
|
</div>
|
|
82
95
|
|
|
83
96
|
{/* Content */}
|
|
@@ -85,181 +98,128 @@ export default async function PrivacyPage() {
|
|
|
85
98
|
{/* Introduction */}
|
|
86
99
|
<section className="mb-10">
|
|
87
100
|
<p className="text-muted-foreground leading-relaxed">
|
|
88
|
-
|
|
89
|
-
security of your personal information. This Privacy Policy explains how we collect,
|
|
90
|
-
use, disclose, and safeguard your information when you visit our website or make
|
|
91
|
-
a purchase.
|
|
101
|
+
{t('privacy.intro', { storeName })}
|
|
92
102
|
</p>
|
|
93
103
|
</section>
|
|
94
104
|
|
|
95
105
|
{/* Information We Collect */}
|
|
96
106
|
<section className="mb-10">
|
|
97
107
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
98
|
-
|
|
108
|
+
{t('privacy.collect.title')}
|
|
99
109
|
</h2>
|
|
100
|
-
<p className="text-muted-foreground mb-4">
|
|
101
|
-
We collect information that you provide directly to us, including:
|
|
102
|
-
</p>
|
|
110
|
+
<p className="text-muted-foreground mb-4">{t('privacy.collect.intro')}</p>
|
|
103
111
|
<ul className="list-disc pl-6 space-y-2 text-muted-foreground">
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<li>Communications you send to us</li>
|
|
108
|
-
<li>Account information if you create an account</li>
|
|
112
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
113
|
+
<li key={i}>{t(`privacy.collect.li${i}`)}</li>
|
|
114
|
+
))}
|
|
109
115
|
</ul>
|
|
110
|
-
<p className="text-muted-foreground mt-4">
|
|
111
|
-
We also automatically collect certain information when you visit our website,
|
|
112
|
-
including your IP address, browser type, device information, and browsing behavior.
|
|
113
|
-
</p>
|
|
116
|
+
<p className="text-muted-foreground mt-4">{t('privacy.collect.auto')}</p>
|
|
114
117
|
</section>
|
|
115
118
|
|
|
116
119
|
{/* How We Use Your Information */}
|
|
117
120
|
<section className="mb-10">
|
|
118
121
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
119
|
-
|
|
122
|
+
{t('privacy.use.title')}
|
|
120
123
|
</h2>
|
|
121
|
-
<p className="text-muted-foreground mb-4">
|
|
122
|
-
We use the information we collect to:
|
|
123
|
-
</p>
|
|
124
|
+
<p className="text-muted-foreground mb-4">{t('privacy.use.intro')}</p>
|
|
124
125
|
<ul className="list-disc pl-6 space-y-2 text-muted-foreground">
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<li>Send promotional communications (with your consent)</li>
|
|
129
|
-
<li>Improve our website and services</li>
|
|
130
|
-
<li>Detect and prevent fraud</li>
|
|
131
|
-
<li>Comply with legal obligations</li>
|
|
126
|
+
{[1, 2, 3, 4, 5, 6, 7].map((i) => (
|
|
127
|
+
<li key={i}>{t(`privacy.use.li${i}`)}</li>
|
|
128
|
+
))}
|
|
132
129
|
</ul>
|
|
133
130
|
</section>
|
|
134
131
|
|
|
135
132
|
{/* Information Sharing */}
|
|
136
133
|
<section className="mb-10">
|
|
137
134
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
138
|
-
|
|
135
|
+
{t('privacy.share.title')}
|
|
139
136
|
</h2>
|
|
140
|
-
<p className="text-muted-foreground mb-4">
|
|
141
|
-
We do not sell your personal information. We may share your information with:
|
|
142
|
-
</p>
|
|
137
|
+
<p className="text-muted-foreground mb-4">{t('privacy.share.intro')}</p>
|
|
143
138
|
<ul className="list-disc pl-6 space-y-2 text-muted-foreground">
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
{[1, 2, 3].map((i) => (
|
|
140
|
+
<li key={i}>{t(`privacy.share.li${i}`)}</li>
|
|
141
|
+
))}
|
|
147
142
|
</ul>
|
|
148
143
|
</section>
|
|
149
144
|
|
|
150
145
|
{/* Data Security */}
|
|
151
146
|
<section className="mb-10">
|
|
152
147
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
153
|
-
|
|
148
|
+
{t('privacy.security.title')}
|
|
154
149
|
</h2>
|
|
155
|
-
<p className="text-muted-foreground">
|
|
156
|
-
We implement appropriate technical and organizational measures to protect your
|
|
157
|
-
personal information against unauthorized access, alteration, disclosure, or
|
|
158
|
-
destruction. All payment transactions are encrypted using SSL technology and
|
|
159
|
-
processed through secure payment providers.
|
|
160
|
-
</p>
|
|
150
|
+
<p className="text-muted-foreground">{t('privacy.security.body')}</p>
|
|
161
151
|
</section>
|
|
162
152
|
|
|
163
153
|
{/* Cookies */}
|
|
164
154
|
<section id="cookies" className="mb-10 scroll-mt-24">
|
|
165
155
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
166
|
-
|
|
156
|
+
{t('privacy.cookies.title')}
|
|
167
157
|
</h2>
|
|
168
|
-
<p className="text-muted-foreground mb-4">
|
|
169
|
-
We use cookies and similar technologies to make this store work
|
|
170
|
-
and, with your consent, to understand how it is used and show
|
|
171
|
-
you relevant content. Cookies are grouped into three categories:
|
|
172
|
-
</p>
|
|
158
|
+
<p className="text-muted-foreground mb-4">{t('privacy.cookies.intro')}</p>
|
|
173
159
|
<ul className="list-disc pl-6 space-y-2 text-muted-foreground">
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<strong className="text-foreground">Analytics.</strong> Help us
|
|
181
|
-
understand how the store is used so we can improve it.
|
|
182
|
-
Optional; off unless you choose to allow them.
|
|
183
|
-
</li>
|
|
184
|
-
<li>
|
|
185
|
-
<strong className="text-foreground">Marketing.</strong> Used to
|
|
186
|
-
measure ads and show you relevant content across sites.
|
|
187
|
-
Optional; off unless you choose to allow them.
|
|
188
|
-
</li>
|
|
160
|
+
{(['essential', 'analytics', 'marketing'] as const).map((cat) => (
|
|
161
|
+
<li key={cat}>
|
|
162
|
+
<strong className="text-foreground">{t(`privacy.cookies.${cat}.label`)}</strong>{' '}
|
|
163
|
+
{t(`privacy.cookies.${cat}.text`)}
|
|
164
|
+
</li>
|
|
165
|
+
))}
|
|
189
166
|
</ul>
|
|
190
|
-
<p className="text-muted-foreground mt-4">
|
|
191
|
-
You can change your preferences at any time by clicking
|
|
192
|
-
“Cookie preferences” in the footer of any page. If
|
|
193
|
-
you reject non-essential cookies, some features that rely on
|
|
194
|
-
analytics or personalization may be limited.
|
|
195
|
-
</p>
|
|
167
|
+
<p className="text-muted-foreground mt-4">{t('privacy.cookies.manage')}</p>
|
|
196
168
|
</section>
|
|
197
169
|
|
|
198
170
|
{/* Your Rights */}
|
|
199
171
|
<section className="mb-10">
|
|
200
172
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
201
|
-
|
|
173
|
+
{t('privacy.rights.title')}
|
|
202
174
|
</h2>
|
|
203
|
-
<p className="text-muted-foreground mb-4">
|
|
204
|
-
Depending on your location, you may have the right to:
|
|
205
|
-
</p>
|
|
175
|
+
<p className="text-muted-foreground mb-4">{t('privacy.rights.intro')}</p>
|
|
206
176
|
<ul className="list-disc pl-6 space-y-2 text-muted-foreground">
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<li>Opt out of marketing communications</li>
|
|
211
|
-
<li>Data portability</li>
|
|
177
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
178
|
+
<li key={i}>{t(`privacy.rights.li${i}`)}</li>
|
|
179
|
+
))}
|
|
212
180
|
</ul>
|
|
213
181
|
</section>
|
|
214
182
|
|
|
215
183
|
{/* Children's Privacy */}
|
|
216
184
|
<section className="mb-10">
|
|
217
185
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
218
|
-
|
|
186
|
+
{t('privacy.children.title')}
|
|
219
187
|
</h2>
|
|
220
|
-
<p className="text-muted-foreground">
|
|
221
|
-
Our website is not intended for children under 13 years of age. We do not
|
|
222
|
-
knowingly collect personal information from children. If you believe we have
|
|
223
|
-
collected information from a child, please contact us immediately.
|
|
224
|
-
</p>
|
|
188
|
+
<p className="text-muted-foreground">{t('privacy.children.body')}</p>
|
|
225
189
|
</section>
|
|
226
190
|
|
|
227
191
|
{/* Changes to Policy */}
|
|
228
192
|
<section className="mb-10">
|
|
229
193
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
230
|
-
|
|
194
|
+
{t('privacy.changes.title')}
|
|
231
195
|
</h2>
|
|
232
|
-
<p className="text-muted-foreground">
|
|
233
|
-
We may update this Privacy Policy from time to time. We will notify you of any
|
|
234
|
-
changes by posting the new policy on this page and updating the "Last updated"
|
|
235
|
-
date. We encourage you to review this policy periodically.
|
|
236
|
-
</p>
|
|
196
|
+
<p className="text-muted-foreground">{t('privacy.changes.body')}</p>
|
|
237
197
|
</section>
|
|
238
198
|
|
|
239
199
|
{/* Contact Us */}
|
|
240
200
|
<section className="mb-10">
|
|
241
201
|
<h2 className="text-xl font-semibold font-heading mb-4">
|
|
242
|
-
|
|
202
|
+
{t('privacy.contact.title')}
|
|
243
203
|
</h2>
|
|
244
204
|
<p className="text-muted-foreground">
|
|
245
|
-
|
|
246
|
-
practices, please
|
|
205
|
+
{t('privacy.contact.lead')}
|
|
247
206
|
{supportEmail ? (
|
|
248
207
|
<>
|
|
249
|
-
{' '}
|
|
208
|
+
{' '}
|
|
209
|
+
{t('legal.contactVia.email')}{' '}
|
|
250
210
|
<a
|
|
251
211
|
href={`mailto:${supportEmail}`}
|
|
252
212
|
className="text-primary hover:underline"
|
|
253
213
|
>
|
|
254
214
|
{supportEmail}
|
|
255
215
|
</a>{' '}
|
|
256
|
-
or
|
|
216
|
+
{t('legal.contactVia.or')}{' '}
|
|
257
217
|
</>
|
|
258
218
|
) : (
|
|
259
|
-
<>{'
|
|
219
|
+
<> {t('legal.contactVia.page')} </>
|
|
260
220
|
)}
|
|
261
221
|
<Link href="/contact" className="text-primary hover:underline">
|
|
262
|
-
|
|
222
|
+
{t('legal.contactPageLink')}
|
|
263
223
|
</Link>
|
|
264
224
|
.
|
|
265
225
|
</p>
|
|
@@ -272,7 +232,7 @@ export default async function PrivacyPage() {
|
|
|
272
232
|
href="/"
|
|
273
233
|
className="text-sm text-primary hover:underline"
|
|
274
234
|
>
|
|
275
|
-
←
|
|
235
|
+
← {t('legal.backToHome')}
|
|
276
236
|
</Link>
|
|
277
237
|
</div>
|
|
278
238
|
</div>
|
|
@@ -62,9 +62,12 @@ export function ProductDetailClient({ slug }: { slug: string }) {
|
|
|
62
62
|
// Generate a key to reset gallery when variant changes
|
|
63
63
|
const galleryKey = currentVariant?.id || 'no-variant'
|
|
64
64
|
|
|
65
|
-
// Determine price and stock (safe if product is null)
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// Determine price and stock (safe if product is null). Prices arrive as
|
|
66
|
+
// DECIMAL STRINGS from the API — coerce to numbers before any comparison
|
|
67
|
+
// ('185.00' > '55.50' is lexicographically FALSE, which hid the
|
|
68
|
+
// strikethrough on every sale product).
|
|
69
|
+
const price = Number(currentVariant?.price ?? product?.price ?? 0)
|
|
70
|
+
const comparePrice = product?.comparePrice != null ? Number(product.comparePrice) : null
|
|
68
71
|
const inStock = currentVariant
|
|
69
72
|
? (currentVariant.inventory ?? 0) > 0
|
|
70
73
|
: true
|
|
@@ -8,9 +8,19 @@
|
|
|
8
8
|
* - ProductSearch: Search input with debouncing
|
|
9
9
|
* - ProductSort: Sort dropdown
|
|
10
10
|
* - useProducts: Data fetching hook
|
|
11
|
+
*
|
|
12
|
+
* URL filters (the contract store-authored nav links rely on):
|
|
13
|
+
* - ?category=<slug|id> — mega-menu / collection links carry slugs; the
|
|
14
|
+
* products API resolves either. Without this read, category links
|
|
15
|
+
* silently showed the full catalog.
|
|
16
|
+
* - ?sale=1 — on-sale products only (comparePrice > price).
|
|
17
|
+
*
|
|
18
|
+
* `useSearchParams` requires a Suspense boundary or the page fails static
|
|
19
|
+
* prerender at build time — hence the thin default-export wrapper.
|
|
11
20
|
*/
|
|
12
21
|
|
|
13
|
-
import { useState } from 'react'
|
|
22
|
+
import { Suspense, useState } from 'react'
|
|
23
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
14
24
|
import {
|
|
15
25
|
ProductGrid,
|
|
16
26
|
ProductSearch,
|
|
@@ -21,7 +31,25 @@ import type { SortOption } from '@rovela-ai/sdk/products'
|
|
|
21
31
|
import { useT } from '@rovela-ai/sdk/core'
|
|
22
32
|
|
|
23
33
|
export default function ProductsPage() {
|
|
34
|
+
return (
|
|
35
|
+
<Suspense fallback={null}>
|
|
36
|
+
<ProductsPageContent />
|
|
37
|
+
</Suspense>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
42
|
+
|
|
43
|
+
function ProductsPageContent() {
|
|
24
44
|
const t = useT()
|
|
45
|
+
const router = useRouter()
|
|
46
|
+
const pathname = usePathname()
|
|
47
|
+
const searchParams = useSearchParams()
|
|
48
|
+
|
|
49
|
+
const category = searchParams.get('category') ?? undefined
|
|
50
|
+
const saleParam = searchParams.get('sale') ?? searchParams.get('onSale')
|
|
51
|
+
const onSale = saleParam === '1' || saleParam === 'true' ? true : undefined
|
|
52
|
+
|
|
25
53
|
const [search, setSearch] = useState('')
|
|
26
54
|
const [sort, setSort] = useState<SortOption>('newest')
|
|
27
55
|
|
|
@@ -34,9 +62,20 @@ export default function ProductsPage() {
|
|
|
34
62
|
} = useProducts({
|
|
35
63
|
search,
|
|
36
64
|
sort,
|
|
65
|
+
category,
|
|
66
|
+
onSale,
|
|
37
67
|
limit: 12,
|
|
38
68
|
})
|
|
39
69
|
|
|
70
|
+
// Chip label: a slug reads fine prettified; a raw UUID falls back to the
|
|
71
|
+
// generic "Category" label.
|
|
72
|
+
const categoryLabel =
|
|
73
|
+
category && !UUID_RE.test(category)
|
|
74
|
+
? category.replace(/[-_]+/g, ' ').replace(/^\w/, (c) => c.toUpperCase())
|
|
75
|
+
: t('listing.category')
|
|
76
|
+
|
|
77
|
+
const clearFilters = () => router.replace(pathname)
|
|
78
|
+
|
|
40
79
|
return (
|
|
41
80
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
42
81
|
{/* Page Header */}
|
|
@@ -61,6 +100,29 @@ export default function ProductsPage() {
|
|
|
61
100
|
/>
|
|
62
101
|
</div>
|
|
63
102
|
|
|
103
|
+
{/* Active URL filters (category link / sale link) */}
|
|
104
|
+
{(category || onSale) && (
|
|
105
|
+
<div className="mb-6 flex flex-wrap items-center gap-2">
|
|
106
|
+
{category && (
|
|
107
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1 text-sm font-medium text-primary">
|
|
108
|
+
{categoryLabel}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
{onSale && (
|
|
112
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 px-3 py-1 text-sm font-medium text-primary">
|
|
113
|
+
{t('listing.onSale')}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={clearFilters}
|
|
119
|
+
className="text-sm text-muted-foreground underline underline-offset-2 hover:text-foreground"
|
|
120
|
+
>
|
|
121
|
+
{t('listing.clearFilter')}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
64
126
|
{/* One message per state: on error, ONLY the error renders (the grid
|
|
65
127
|
is skipped — its built-in empty state would double the message);
|
|
66
128
|
when empty, the page's copy rides the grid's emptyState slot. */}
|