@rovela-ai/sdk 0.20.0 → 0.22.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.
Files changed (140) hide show
  1. package/dist/admin/api/index.d.ts +2 -0
  2. package/dist/admin/api/index.d.ts.map +1 -1
  3. package/dist/admin/api/index.js +2 -0
  4. package/dist/admin/api/index.js.map +1 -1
  5. package/dist/admin/api/integrations.d.ts +60 -0
  6. package/dist/admin/api/integrations.d.ts.map +1 -0
  7. package/dist/admin/api/integrations.js +337 -0
  8. package/dist/admin/api/integrations.js.map +1 -0
  9. package/dist/admin/api/products.d.ts.map +1 -1
  10. package/dist/admin/api/products.js +4 -0
  11. package/dist/admin/api/products.js.map +1 -1
  12. package/dist/admin/api/tax-zones.d.ts.map +1 -1
  13. package/dist/admin/api/tax-zones.js +7 -0
  14. package/dist/admin/api/tax-zones.js.map +1 -1
  15. package/dist/admin/components/AdminNav.d.ts.map +1 -1
  16. package/dist/admin/components/AdminNav.js +3 -1
  17. package/dist/admin/components/AdminNav.js.map +1 -1
  18. package/dist/admin/components/AdminSelect.d.ts.map +1 -1
  19. package/dist/admin/components/AdminSelect.js +2 -0
  20. package/dist/admin/components/AdminSelect.js.map +1 -1
  21. package/dist/admin/components/DiscountsManager.d.ts.map +1 -1
  22. package/dist/admin/components/DiscountsManager.js +2 -2
  23. package/dist/admin/components/DiscountsManager.js.map +1 -1
  24. package/dist/admin/components/IntegrationsManager.d.ts +5 -0
  25. package/dist/admin/components/IntegrationsManager.d.ts.map +1 -0
  26. package/dist/admin/components/IntegrationsManager.js +138 -0
  27. package/dist/admin/components/IntegrationsManager.js.map +1 -0
  28. package/dist/admin/components/OrderDetails.d.ts.map +1 -1
  29. package/dist/admin/components/OrderDetails.js +12 -4
  30. package/dist/admin/components/OrderDetails.js.map +1 -1
  31. package/dist/admin/components/ProductForm.d.ts.map +1 -1
  32. package/dist/admin/components/ProductForm.js +25 -1
  33. package/dist/admin/components/ProductForm.js.map +1 -1
  34. package/dist/admin/components/ShippingSettings.d.ts.map +1 -1
  35. package/dist/admin/components/ShippingSettings.js +21 -21
  36. package/dist/admin/components/ShippingSettings.js.map +1 -1
  37. package/dist/admin/components/TaxSettings.d.ts.map +1 -1
  38. package/dist/admin/components/TaxSettings.js +14 -2
  39. package/dist/admin/components/TaxSettings.js.map +1 -1
  40. package/dist/admin/components/UsersTable.d.ts.map +1 -1
  41. package/dist/admin/components/UsersTable.js +2 -0
  42. package/dist/admin/components/UsersTable.js.map +1 -1
  43. package/dist/admin/components/VariantManager.d.ts.map +1 -1
  44. package/dist/admin/components/VariantManager.js +8 -2
  45. package/dist/admin/components/VariantManager.js.map +1 -1
  46. package/dist/admin/components/index.d.ts +2 -0
  47. package/dist/admin/components/index.d.ts.map +1 -1
  48. package/dist/admin/components/index.js +1 -0
  49. package/dist/admin/components/index.js.map +1 -1
  50. package/dist/admin/index.d.ts +1 -1
  51. package/dist/admin/index.d.ts.map +1 -1
  52. package/dist/admin/index.js +2 -0
  53. package/dist/admin/index.js.map +1 -1
  54. package/dist/admin/integrations/brevo-logo.d.ts +11 -0
  55. package/dist/admin/integrations/brevo-logo.d.ts.map +1 -0
  56. package/dist/admin/integrations/brevo-logo.js +11 -0
  57. package/dist/admin/integrations/brevo-logo.js.map +1 -0
  58. package/dist/admin/integrations/registry.d.ts +116 -0
  59. package/dist/admin/integrations/registry.d.ts.map +1 -0
  60. package/dist/admin/integrations/registry.js +389 -0
  61. package/dist/admin/integrations/registry.js.map +1 -0
  62. package/dist/admin/types.d.ts +15 -0
  63. package/dist/admin/types.d.ts.map +1 -1
  64. package/dist/checkout/api/manual.d.ts.map +1 -1
  65. package/dist/checkout/api/manual.js +7 -0
  66. package/dist/checkout/api/manual.js.map +1 -1
  67. package/dist/checkout/api/shipping.d.ts +0 -18
  68. package/dist/checkout/api/shipping.d.ts.map +1 -1
  69. package/dist/checkout/api/shipping.js +33 -3
  70. package/dist/checkout/api/shipping.js.map +1 -1
  71. package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
  72. package/dist/checkout/components/CheckoutFlow.js +30 -14
  73. package/dist/checkout/components/CheckoutFlow.js.map +1 -1
  74. package/dist/checkout/components/OrderSummary.d.ts +1 -1
  75. package/dist/checkout/components/OrderSummary.d.ts.map +1 -1
  76. package/dist/checkout/components/OrderSummary.js +5 -2
  77. package/dist/checkout/components/OrderSummary.js.map +1 -1
  78. package/dist/checkout/server/create-checkout-session.d.ts.map +1 -1
  79. package/dist/checkout/server/create-checkout-session.js +38 -9
  80. package/dist/checkout/server/create-checkout-session.js.map +1 -1
  81. package/dist/checkout/server/handle-webhook.js +6 -0
  82. package/dist/checkout/server/handle-webhook.js.map +1 -1
  83. package/dist/checkout/server/order-service.d.ts.map +1 -1
  84. package/dist/checkout/server/order-service.js +21 -0
  85. package/dist/checkout/server/order-service.js.map +1 -1
  86. package/dist/checkout/server/pricing.d.ts +28 -1
  87. package/dist/checkout/server/pricing.d.ts.map +1 -1
  88. package/dist/checkout/server/pricing.js +84 -6
  89. package/dist/checkout/server/pricing.js.map +1 -1
  90. package/dist/checkout/types.d.ts +32 -0
  91. package/dist/checkout/types.d.ts.map +1 -1
  92. package/dist/core/db/queries.d.ts +83 -2
  93. package/dist/core/db/queries.d.ts.map +1 -1
  94. package/dist/core/db/queries.js +88 -2
  95. package/dist/core/db/queries.js.map +1 -1
  96. package/dist/core/db/schema.d.ts +65 -0
  97. package/dist/core/db/schema.d.ts.map +1 -1
  98. package/dist/core/db/schema.js +26 -0
  99. package/dist/core/db/schema.js.map +1 -1
  100. package/dist/core/i18n/messages/en.d.ts.map +1 -1
  101. package/dist/core/i18n/messages/en.js +123 -0
  102. package/dist/core/i18n/messages/en.js.map +1 -1
  103. package/dist/core/i18n/messages/fr.d.ts.map +1 -1
  104. package/dist/core/i18n/messages/fr.js +123 -0
  105. package/dist/core/i18n/messages/fr.js.map +1 -1
  106. package/dist/emails/send/orders.d.ts +4 -0
  107. package/dist/emails/send/orders.d.ts.map +1 -1
  108. package/dist/emails/send/orders.js +1 -0
  109. package/dist/emails/send/orders.js.map +1 -1
  110. package/dist/emails/templates/order-confirmation.d.ts +9 -0
  111. package/dist/emails/templates/order-confirmation.d.ts.map +1 -1
  112. package/dist/emails/templates/order-confirmation.js +19 -8
  113. package/dist/emails/templates/order-confirmation.js.map +1 -1
  114. package/dist/emails/types.d.ts +5 -0
  115. package/dist/emails/types.d.ts.map +1 -1
  116. package/dist/products/api/products.d.ts +3 -1
  117. package/dist/products/api/products.d.ts.map +1 -1
  118. package/dist/products/api/products.js +44 -3
  119. package/dist/products/api/products.js.map +1 -1
  120. package/dist/products/components/VariantSelector.d.ts.map +1 -1
  121. package/dist/products/components/VariantSelector.js +4 -1
  122. package/dist/products/components/VariantSelector.js.map +1 -1
  123. package/dist/products/hooks/useProducts.d.ts +3 -1
  124. package/dist/products/hooks/useProducts.d.ts.map +1 -1
  125. package/dist/products/hooks/useProducts.js +4 -2
  126. package/dist/products/hooks/useProducts.js.map +1 -1
  127. package/docs/changelog/epoch-08-integrations.md +125 -0
  128. package/docs/changelog/epoch-09-catalog-tax.md +91 -0
  129. package/package.json +1 -1
  130. package/templates/store-template/.claude/skills/build-feature/SKILL.md +19 -0
  131. package/templates/store-template/.claude/skills/manage-products/SKILL.md +1 -1
  132. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +19 -4
  133. package/templates/store-template/app/admin/(dashboard)/integrations/page.tsx +17 -0
  134. package/templates/store-template/app/api/admin/integrations/route.ts +9 -0
  135. package/templates/store-template/app/faq/page.tsx +42 -56
  136. package/templates/store-template/app/privacy/page.tsx +65 -105
  137. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +6 -3
  138. package/templates/store-template/app/products/page.tsx +63 -1
  139. package/templates/store-template/app/terms/page.tsx +70 -198
  140. 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 { getBlueprint, localizedAlternates, findSettings } from '@rovela-ai/sdk/core/server'
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
- * Static privacy policy page with standard e-commerce sections.
9
- * Store name is pulled from blueprint for personalization.
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: 'Privacy Policy',
27
- description: `Privacy Policy for ${storeName}. Learn how we collect, use, and protect your personal information.`,
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">Privacy Policy</h1>
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
- At {storeName}, we are committed to protecting your privacy and ensuring the
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
- Information We Collect
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
- <li>Name and contact information (email address, phone number, shipping address)</li>
105
- <li>Payment information (processed securely through our payment provider)</li>
106
- <li>Order history and preferences</li>
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
- How We Use Your Information
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
- <li>Process and fulfill your orders</li>
126
- <li>Send you order confirmations and shipping updates</li>
127
- <li>Respond to your questions and provide customer support</li>
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
- Information Sharing
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
- <li>Service providers who assist in our operations (payment processors, shipping carriers)</li>
145
- <li>Analytics providers to help us understand website usage</li>
146
- <li>Legal authorities when required by law</li>
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
- Data Security
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
- Cookies and Tracking
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
- <li>
175
- <strong className="text-foreground">Essential.</strong> Required
176
- for the store to work — including your cart, session, and
177
- sign-in state. Always on; cannot be disabled.
178
- </li>
179
- <li>
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
- &ldquo;Cookie preferences&rdquo; 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
- Your Rights
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
- <li>Access the personal information we hold about you</li>
208
- <li>Request correction of inaccurate information</li>
209
- <li>Request deletion of your personal information</li>
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
- Children&apos;s Privacy
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
- Changes to This Policy
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 &quot;Last updated&quot;
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
- Contact Us
202
+ {t('privacy.contact.title')}
243
203
  </h2>
244
204
  <p className="text-muted-foreground">
245
- If you have any questions about this Privacy Policy or our privacy
246
- practices, please
205
+ {t('privacy.contact.lead')}
247
206
  {supportEmail ? (
248
207
  <>
249
- {' '}contact us at{' '}
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 visit our{' '}
216
+ {t('legal.contactVia.or')}{' '}
257
217
  </>
258
218
  ) : (
259
- <>{' '}reach us through our{' '}</>
219
+ <> {t('legal.contactVia.page')} </>
260
220
  )}
261
221
  <Link href="/contact" className="text-primary hover:underline">
262
- contact page
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
- &larr; Back to Home
235
+ &larr; {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
- const price = currentVariant?.price ?? product?.price ?? 0
67
- const comparePrice = product?.comparePrice
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. */}