@reblu/site-blocks 0.21.0 → 0.23.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/CHANGELOG.md +33 -0
- package/README.md +125 -0
- package/dist/index.cjs +367 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -2
- package/dist/index.d.ts +101 -2
- package/dist/index.js +363 -178
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.23.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f2ed9e2: Default Spanish messages for the new checkout `ShopErrorCode`s (REB-1197, REB-1198): `PICKUP_UNAVAILABLE`, `PICKUP_LOCATION_INVALID`, `SHIPPING_ADDRESS_REQUIRED`, `ORDER_TOO_HEAVY`, `TERMS_NOT_ACCEPTED`, `TERMS_VERSION_CHANGED` and `DELIVERY_UNAVAILABLE`, in `DEFAULT_SHOP_ERROR_MESSAGES` (override any of them with `getShopErrorMessage`'s `overrides`).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [f2ed9e2]
|
|
12
|
+
- @reblu/site-contracts@1.23.0
|
|
13
|
+
|
|
14
|
+
## 0.22.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 01d0e15: Academy catalogue filters by price and instructor (REB-547). `@reblu/site-contracts` exports `AcademyCourseListQuery`, the documented query of `GET /api/site/academy/courses`. `@reblu/site-client` adds `academy.instructors()` (instructors of published courses, for the filter), and `AcademyCourseListParams` is now that query type, so the SDK cannot drift from the API. `@reblu/site-blocks` adds `CourseCatalogFilters`, a drop-in instructor and price filter (bands in integer cents plus «Gratis»), RSC-safe like `ShopFilters`.
|
|
19
|
+
- d1db04e: Tenant-configurable interest question on the contact form (REB-759).
|
|
20
|
+
- `Config.contactInterest`: `{ label, options }` or `null`, the tenant's own question and options from nexus. `null` means the field must not render; there is no default catalogue.
|
|
21
|
+
- `ContactFormInput.interests`: the chosen option labels (up to 20). The server keeps only labels the tenant offers.
|
|
22
|
+
- New drop-in `ContactInterestField` in `@reblu/site-blocks`: a controlled checkbox group that renders nothing when the tenant has no question. Slots: `contact-interest`, `contact-interest-option`.
|
|
23
|
+
|
|
24
|
+
- 5ca48ee: Discount code usage limits are enforced before payment (REB-912).
|
|
25
|
+
- New `ShopErrorCode` `DISCOUNT_LIMIT_REACHED` (400): checkout refuses a discount code that has used up its total uses (slots held by open payment sessions count), before any payment session is created. The buyer can continue without the code.
|
|
26
|
+
- `DEFAULT_SHOP_ERROR_MESSAGES.DISCOUNT_LIMIT_REACHED`: «Este código ya ha alcanzado su límite de usos».
|
|
27
|
+
- A per-customer limit still answers `DISCOUNT_INVALID` with a neutral message.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [01d0e15]
|
|
32
|
+
- Updated dependencies [d1db04e]
|
|
33
|
+
- Updated dependencies [5ca48ee]
|
|
34
|
+
- @reblu/site-contracts@1.22.0
|
|
35
|
+
|
|
3
36
|
## 0.21.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ colour on the red background. See `docs/theming/theming.md`.
|
|
|
29
29
|
| `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
|
|
30
30
|
| `ProfessionalFields` | `.` | The tenant's own professional fields (`professional.fields`) on a team card or profile. RSC-safe. See below. |
|
|
31
31
|
| `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
|
|
32
|
+
| `CourseCatalogFilters`, `DEFAULT_COURSE_PRICE_BANDS` | `.` | Course catalogue filters by instructor and price (`client.academy.instructors()`). RSC-safe. See "Course catalogue filters". |
|
|
32
33
|
| `CartLineList`, `CartSummary`, `CartAdjustmentsNotice`, `AddedToCartToast`, `CheckoutLoginPrompt`, `CheckoutForm`, `OrderConfirmation` | `.` | Cart & checkout organisms. RSC-safe by construction. See "Cart & checkout" below. |
|
|
33
34
|
| `PageBlocks`, `FaqAccordion` | `.` | Registry-block renderer (`PageDetail.blocks`) and native `<details>` FAQ. Server Components. See "Page blocks" below. |
|
|
34
35
|
| `ProductTrustItems`, `DEFAULT_TRUST_ICONS` | `.` | Store-wide trust messages from `ShopConfig.productTrustItems`. Server Component. See "Product trust items" below. |
|
|
@@ -634,6 +635,27 @@ NIF/NIE or ID document, name and billing address), `INVOICING_NOT_READY`
|
|
|
634
635
|
(503) and `FULL_INVOICE_UNAVAILABLE` (422 — `wantsFullInvoice: true` sent to a
|
|
635
636
|
shop that issues no invoices).
|
|
636
637
|
|
|
638
|
+
A discount code that has used up its total uses is refused by checkout
|
|
639
|
+
BEFORE any payment session exists, with `DISCOUNT_LIMIT_REACHED` (400, REB-912)
|
|
640
|
+
and the default copy «Este código ya ha alcanzado su límite de usos». Let the
|
|
641
|
+
buyer remove the code and continue; nothing was charged. Slots held by open
|
|
642
|
+
payment sessions count, so the last use can never be sold twice. A
|
|
643
|
+
per-customer limit keeps answering `DISCOUNT_INVALID` with a neutral message,
|
|
644
|
+
so the response never reveals whether an email already bought.
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
import { getShopErrorMessage } from '@reblu/site-blocks'
|
|
648
|
+
import { ApiError } from '@reblu/site-client'
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await client.shop.checkout(body)
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (error instanceof ApiError && error.body?.code === 'DISCOUNT_LIMIT_REACHED') {
|
|
654
|
+
setDiscountError(getShopErrorMessage('DISCOUNT_LIMIT_REACHED')) // offer «continuar sin código»
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
637
659
|
## Product trust items
|
|
638
660
|
|
|
639
661
|
`ProductTrustItems` renders the store-wide trust messages (shipping, returns,
|
|
@@ -969,6 +991,78 @@ import { CourseSyllabus } from '@reblu/site-blocks/client'
|
|
|
969
991
|
and `course-syllabus-preview`. No slot was renamed in REB-1135. It renders
|
|
970
992
|
nothing for a course without modules.
|
|
971
993
|
|
|
994
|
+
## Course catalogue filters — `CourseCatalogFilters` (REB-547)
|
|
995
|
+
|
|
996
|
+
Drop-in filters for the academy catalogue: an instructor filter fed by
|
|
997
|
+
`client.academy.instructors()` (instructors of published courses, sorted by
|
|
998
|
+
name) and a price filter (bands in integer cents plus «Gratis»). Each option
|
|
999
|
+
maps 1:1 onto a `client.academy.courses()` param: `instructor`, `priceMin`,
|
|
1000
|
+
`priceMax`, `free`. Same RSC-safe contract as `ShopFilters`: every option is a
|
|
1001
|
+
real `<a href>`; pass `onChange` only from a `'use client'` boundary.
|
|
1002
|
+
|
|
1003
|
+
```tsx
|
|
1004
|
+
import { CourseCatalogFilters } from '@reblu/site-blocks'
|
|
1005
|
+
import type { AcademyCourseListQuery } from '@reblu/site-contracts'
|
|
1006
|
+
import { client } from '@/lib/reblu'
|
|
1007
|
+
|
|
1008
|
+
export default async function CoursesPage({
|
|
1009
|
+
searchParams,
|
|
1010
|
+
}: {
|
|
1011
|
+
searchParams: Promise<Record<string, string | undefined>>
|
|
1012
|
+
}) {
|
|
1013
|
+
const sp = await searchParams
|
|
1014
|
+
const num = (v?: string) => (v ? Number(v) : undefined)
|
|
1015
|
+
const active: AcademyCourseListQuery = {
|
|
1016
|
+
instructor: sp.instructor,
|
|
1017
|
+
priceMin: num(sp.priceMin),
|
|
1018
|
+
priceMax: num(sp.priceMax),
|
|
1019
|
+
free: sp.free === 'true',
|
|
1020
|
+
}
|
|
1021
|
+
const [courses, instructors] = await Promise.all([
|
|
1022
|
+
client.academy.courses(active),
|
|
1023
|
+
client.academy.instructors(),
|
|
1024
|
+
])
|
|
1025
|
+
|
|
1026
|
+
// Keep the other params; `free` and a price band exclude each other.
|
|
1027
|
+
const href = (patch: Partial<AcademyCourseListQuery>) => {
|
|
1028
|
+
const next = new URLSearchParams()
|
|
1029
|
+
for (const [k, v] of Object.entries({ ...active, ...patch })) {
|
|
1030
|
+
if (v !== undefined && v !== false) next.set(k, String(v))
|
|
1031
|
+
}
|
|
1032
|
+
const qs = next.toString()
|
|
1033
|
+
return qs ? `/cursos?${qs}` : '/cursos'
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return (
|
|
1037
|
+
<>
|
|
1038
|
+
<CourseCatalogFilters
|
|
1039
|
+
instructors={instructors.data}
|
|
1040
|
+
active={active}
|
|
1041
|
+
hrefs={{
|
|
1042
|
+
instructor: (id) => href({ instructor: id }),
|
|
1043
|
+
price: (min, max) => href({ priceMin: min, priceMax: max, free: false }),
|
|
1044
|
+
free: (next) => href({ free: next, priceMin: undefined, priceMax: undefined }),
|
|
1045
|
+
clearAll: () => '/cursos',
|
|
1046
|
+
}}
|
|
1047
|
+
/>
|
|
1048
|
+
{/* render courses.data */}
|
|
1049
|
+
</>
|
|
1050
|
+
)
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
- **Price is integer cents.** The default bands (`DEFAULT_COURSE_PRICE_BANDS`)
|
|
1055
|
+
are EUR: up to 50, 50 to 150, 150 to 300, from 300. Pass `priceBands` for
|
|
1056
|
+
another catalogue and `currencyCode` / `locale` for the labels.
|
|
1057
|
+
- **«Gratis» wins.** The API applies `free=true` over any price range, so no
|
|
1058
|
+
band shows as active while it is on; the component's `onChange` patches turn
|
|
1059
|
+
one off when the other is picked, and `hrefs` should do the same.
|
|
1060
|
+
- **Unnamed instructors** (no first or last name) are not offered. An active
|
|
1061
|
+
instructor id that is no longer listed keeps the group visible, so it can be
|
|
1062
|
+
cleared.
|
|
1063
|
+
- Spanish default labels (`DEFAULT_COURSE_CATALOG_FILTERS_LABELS`), all
|
|
1064
|
+
overridable through `labels`.
|
|
1065
|
+
|
|
972
1066
|
## Free-preview lesson — `LessonPreview` (REB-1135)
|
|
973
1067
|
|
|
974
1068
|
A lesson the owner marked «Vista previa gratuita», for visitors who have not
|
|
@@ -1171,6 +1265,37 @@ renderer (`headingOffset` demotes its headings under yours).
|
|
|
1171
1265
|
`markdownToPlainText` drops images and anything the sanitiser strips, and cuts
|
|
1172
1266
|
at a word boundary with `…` within `maxLength`.
|
|
1173
1267
|
|
|
1268
|
+
## Contact form interest question — `ContactInterestField` (REB-759)
|
|
1269
|
+
|
|
1270
|
+
The tenant's own optional question on the contact form, e.g. «¿Qué tratamiento
|
|
1271
|
+
te interesa?», with the options it configured in nexus (Integraciones →
|
|
1272
|
+
Formulario de Contacto). Pass `config.contactInterest` from `/api/site/config`
|
|
1273
|
+
and send the chosen labels as `interests`. Unconfigured (`null`, or an older
|
|
1274
|
+
base-api) renders nothing. **Never supply your own fallback list**: there is no
|
|
1275
|
+
default catalogue, and the server keeps only labels the tenant offers.
|
|
1276
|
+
|
|
1277
|
+
```tsx
|
|
1278
|
+
'use client'
|
|
1279
|
+
import { useState } from 'react'
|
|
1280
|
+
import { ContactInterestField } from '@reblu/site-blocks'
|
|
1281
|
+
import type { Config } from '@reblu/site-contracts'
|
|
1282
|
+
|
|
1283
|
+
export function ContactFormInterest({ config }: { config: Config }) {
|
|
1284
|
+
const [interests, setInterests] = useState<string[]>([])
|
|
1285
|
+
// ...on submit: client.contactForm.submit({ ...fields, interests })
|
|
1286
|
+
return (
|
|
1287
|
+
<ContactInterestField
|
|
1288
|
+
interest={config.contactInterest}
|
|
1289
|
+
value={interests}
|
|
1290
|
+
onChange={setInterests}
|
|
1291
|
+
/>
|
|
1292
|
+
)
|
|
1293
|
+
}
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
Styling: `data-slot="contact-interest"` (the `<fieldset>`, its `<legend>` is the
|
|
1297
|
+
question) and `data-slot="contact-interest-option"` (each `<label>`).
|
|
1298
|
+
|
|
1174
1299
|
## GDPR rights form — `DataRequestForm` (REB-980)
|
|
1175
1300
|
|
|
1176
1301
|
Drop-in form for a SITE's public rights page (e.g. `/derechos-rgpd`): request
|