@reblu/site-next 1.8.0 → 1.10.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 +51 -0
- package/README.md +31 -8
- package/dist/academy-server.cjs +52 -0
- package/dist/academy-server.cjs.map +1 -0
- package/dist/academy-server.d.cts +84 -0
- package/dist/academy-server.d.ts +84 -0
- package/dist/academy-server.js +25 -0
- package/dist/academy-server.js.map +1 -0
- package/dist/academy.cjs.map +1 -1
- package/dist/academy.d.cts +3 -82
- package/dist/academy.d.ts +3 -82
- package/dist/academy.js.map +1 -1
- package/dist/consent-server.cjs +88 -0
- package/dist/consent-server.cjs.map +1 -0
- package/dist/consent-server.d.cts +22 -0
- package/dist/consent-server.d.ts +22 -0
- package/dist/consent-server.js +58 -0
- package/dist/consent-server.js.map +1 -0
- package/dist/consent-shared-DbdhEXW2.d.cts +71 -0
- package/dist/consent-shared-DbdhEXW2.d.ts +71 -0
- package/dist/consent.cjs +404 -0
- package/dist/consent.cjs.map +1 -0
- package/dist/consent.d.cts +172 -0
- package/dist/consent.d.ts +172 -0
- package/dist/consent.js +362 -0
- package/dist/consent.js.map +1 -0
- package/dist/site-manifest.cjs +48 -0
- package/dist/site-manifest.cjs.map +1 -0
- package/dist/site-manifest.d.cts +50 -0
- package/dist/site-manifest.d.ts +50 -0
- package/dist/site-manifest.js +23 -0
- package/dist/site-manifest.js.map +1 -0
- package/package.json +45 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @reblu/site-next
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2e53bf9: Add `@reblu/site-next/consent` and `@reblu/site-next/consent-server` (REB-642): a cookie-consent mechanism for Reblu SITEs — `<ConsentProvider>`/`useConsent()`, `<ConsentGate>`, `<CookieBanner>` (accept/reject/configure, three equal-weight buttons + an accessible preferences dialog), `<CookiePreferencesTrigger>` for permanent withdrawal, and `<ConsentedAnalytics>` wrapping `<AnalyticsScripts>` behind the `analytics` category. Every non-necessary category defaults to denied, and a stored choice is re-asked automatically when the tenant's cookie-policy content hash changes. `./consent-server` is a dependency-free, isomorphic module (cookie name, category taxonomy, fail-closed cookie parsing, `cookiePolicyVersion()` content hash).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [34b15c4]
|
|
12
|
+
- @reblu/site-contracts@1.2.0
|
|
13
|
+
|
|
14
|
+
## 1.9.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 16dfaea: Add `createSiteManifestRoute` (REB-777), a new `./site-manifest` subpath.
|
|
19
|
+
|
|
20
|
+
A Route Handler factory for a SITE's capability manifest, same isolation
|
|
21
|
+
pattern as `./route-handlers` (REB-510): its own tsup entry, zero
|
|
22
|
+
dependencies (not even `next/server`), no `'use client'`, so importing it
|
|
23
|
+
from `app/.well-known/reblu-site.json/route.ts` never drags in any
|
|
24
|
+
client-component graph.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
export const GET = createSiteManifestRoute({
|
|
28
|
+
surfaces: { 'academy.enrollment': { path: '/academia/inscripcion' } },
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Serves `{ manifestVersion: 1, sdkVersion?, surfaces }` as JSON with a short
|
|
33
|
+
public `Cache-Control` (this payload holds no tenant secrets). `surfaces`
|
|
34
|
+
keys are typed against `@reblu/site-contracts`'s new
|
|
35
|
+
`SiteCapabilitySurfaceKey` — a typo in a surface key is a compile error, not
|
|
36
|
+
a silent no-op.
|
|
37
|
+
|
|
38
|
+
Purely additive — a new export, nothing existing changed.
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- d4dd66d: Fix `deriveValidationFields` throwing when called from a Server Component.
|
|
43
|
+
`@reblu/site-next/academy` carries a leading `'use client'` directive so
|
|
44
|
+
`AcademyEnrollmentForm` survives the build, which made the whole entry —
|
|
45
|
+
including the pure, dependency-free `deriveValidationFields` helper — look
|
|
46
|
+
client-only to Next's RSC boundary. Add `@reblu/site-next/academy-server`, a
|
|
47
|
+
new server-safe subpath exporting only `deriveValidationFields` and its
|
|
48
|
+
types, for Server Components to import from. `@reblu/site-next/academy`
|
|
49
|
+
still re-exports `deriveValidationFields` unchanged for existing consumers
|
|
50
|
+
and client-side call sites.
|
|
51
|
+
- Updated dependencies [16dfaea]
|
|
52
|
+
- @reblu/site-contracts@1.1.0
|
|
53
|
+
|
|
3
54
|
## 1.8.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -18,9 +18,10 @@ intentionally Next-coupled so the lower SDK layers —
|
|
|
18
18
|
5. **reCAPTCHA v3** (`./recaptcha`) — the SITE's half of the contact-form
|
|
19
19
|
captcha contract: the canonical action, the render gate, and the
|
|
20
20
|
interaction-mount key. See "reCAPTCHA v3" below.
|
|
21
|
-
6. **Academy** (`./academy`) — `AcademyEnrollmentForm`,
|
|
22
|
-
enrollment-request form, plus `deriveValidationFields` to
|
|
23
|
-
per-tenant validation fields from course/config data. See
|
|
21
|
+
6. **Academy** (`./academy`, `./academy-server`) — `AcademyEnrollmentForm`,
|
|
22
|
+
the anonymous enrollment-request form, plus `deriveValidationFields` to
|
|
23
|
+
build its per-tenant validation fields from course/config data. See
|
|
24
|
+
"Academy" below.
|
|
24
25
|
|
|
25
26
|
## Install
|
|
26
27
|
|
|
@@ -155,10 +156,24 @@ module load.
|
|
|
155
156
|
## Academy
|
|
156
157
|
|
|
157
158
|
`@reblu/site-next/academy` exports the anonymous academy enrollment-request
|
|
158
|
-
form and the pure helper that derives its
|
|
159
|
+
form (`AcademyEnrollmentForm`) and re-exports the pure helper that derives its
|
|
160
|
+
validation fields from tenant data (`deriveValidationFields`).
|
|
161
|
+
`@reblu/site-next/academy-server` exports ONLY that pure helper.
|
|
162
|
+
|
|
163
|
+
**Which subpath to import from matters.** `./academy` carries a leading
|
|
164
|
+
`'use client'` directive — required so `AcademyEnrollmentForm` survives the
|
|
165
|
+
build — and the whole entry (helper included) is one directive-covered chunk.
|
|
166
|
+
Calling `deriveValidationFields` imported from `./academy` inside a Server
|
|
167
|
+
Component throws at runtime ("Attempted to call deriveValidationFields() from
|
|
168
|
+
the server but deriveValidationFields is on the client"), because Next's RSC
|
|
169
|
+
boundary sees the whole entry as client-only. A course detail page is
|
|
170
|
+
typically an async Server Component computing `validationFields` to pass down
|
|
171
|
+
as a prop — exactly the case that needs the server-safe subpath:
|
|
159
172
|
|
|
160
173
|
```tsx
|
|
161
|
-
|
|
174
|
+
// app/academy/[slug]/page.tsx — an async Server Component
|
|
175
|
+
import { AcademyEnrollmentForm } from '@reblu/site-next/academy'
|
|
176
|
+
import { deriveValidationFields } from '@reblu/site-next/academy-server'
|
|
162
177
|
|
|
163
178
|
// `course` is whatever `GET /api/site/academy/courses/[slug]` (via
|
|
164
179
|
// `@reblu/site-client`'s `academy.bySlug`) already returned — this form never
|
|
@@ -174,9 +189,17 @@ const validationFields = deriveValidationFields(course)
|
|
|
174
189
|
)}
|
|
175
190
|
```
|
|
176
191
|
|
|
177
|
-
`
|
|
178
|
-
|
|
179
|
-
`
|
|
192
|
+
Import `deriveValidationFields` from `./academy` instead only when the call
|
|
193
|
+
site is already inside a `'use client'` boundary (e.g. computing it alongside
|
|
194
|
+
other client-side state) — `./academy` still re-exports it for that case and
|
|
195
|
+
for call sites that prefer one import statement for both.
|
|
196
|
+
|
|
197
|
+
`enrollmentRequestUrl` is a SITE-owned Route Handler path — `/api/academy/
|
|
198
|
+
enrollment-requests` above is illustrative; the SITE chooses its own local
|
|
199
|
+
route. The browser can never call base-api directly (`X-Reblu-Api-Key` is a
|
|
200
|
+
server secret), so that local route proxies to the platform endpoint,
|
|
201
|
+
`POST /api/site/academy/enrollment-requests`. Wire it with `createProxyRoute`
|
|
202
|
+
from `./route-handlers`:
|
|
180
203
|
|
|
181
204
|
```ts
|
|
182
205
|
// app/api/academy/enrollment-requests/route.ts
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/academy/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
deriveValidationFields: () => deriveValidationFields
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(server_exports);
|
|
26
|
+
|
|
27
|
+
// src/academy/derive-validation-fields.ts
|
|
28
|
+
var DEFAULT_GENERIC_VALIDATION_KEY = "validationField";
|
|
29
|
+
var DEFAULT_GENERIC_VALIDATION_LABEL = "Dato de verificaci\xF3n";
|
|
30
|
+
function deriveValidationFields(source, options = {}) {
|
|
31
|
+
if (!source.requiresValidation) return [];
|
|
32
|
+
if (source.validationFields && source.validationFields.length > 0) {
|
|
33
|
+
return source.validationFields.map((field) => ({
|
|
34
|
+
key: field.key,
|
|
35
|
+
label: field.label,
|
|
36
|
+
required: field.required ?? true,
|
|
37
|
+
...field.maxLength !== void 0 ? { maxLength: field.maxLength } : {}
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,
|
|
43
|
+
label: source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,
|
|
44
|
+
required: source.validationFieldRequired ?? true
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
49
|
+
0 && (module.exports = {
|
|
50
|
+
deriveValidationFields
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=academy-server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/academy/server.ts","../src/academy/derive-validation-fields.ts"],"sourcesContent":["/**\n * `@reblu/site-next/academy-server` — the server-safe half of the academy SDK\n * surface (defect fix, follow-up to REB-584/#1095).\n *\n * `deriveValidationFields` is a pure, dependency-free helper (no React, no\n * `@reblu/site-ui`) — exactly the kind of thing a Server Component should be\n * able to call directly, and the README/demo-site usage pattern always called\n * it inline in an async Server Component. But `./academy` (`src/academy/index.ts`)\n * carries its OWN leading `'use client'` directive so `AcademyEnrollmentForm`\n * survives the build (R4 — see `tsup.config.ts`), and with `splitting: false`\n * the whole entry is ONE self-contained chunk sharing that single directive\n * prologue. That made every import from `./academy`, including this pure\n * helper, look like a client-only export to Next's RSC boundary — calling\n * `deriveValidationFields` from a Server Component threw \"Attempted to call\n * deriveValidationFields() from the server but deriveValidationFields is on\n * the client\".\n *\n * This module is its OWN tsup entry (subpath `./academy-server`) with NO\n * React import of any kind, so it must NEVER carry `'use client'` — same\n * direction as `./route-handlers` and `./recaptcha`. Server Components MUST\n * import `deriveValidationFields` from here, not from `./academy`; Client\n * Components (or anywhere already inside a `'use client'` boundary) may keep\n * using either subpath — `./academy` still re-exports it for callers that\n * import it alongside `AcademyEnrollmentForm` in one statement.\n */\nexport {\n deriveValidationFields,\n type AcademyValidationField,\n type AcademyValidationFieldConfig,\n type AcademyValidationSource,\n type DeriveValidationFieldsOptions,\n} from './derive-validation-fields'\n","/**\n * `@reblu/site-next/academy` (also re-exported from the server-safe\n * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-\n * field derivation (REB-584 follow-up).\n *\n * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks\n * for extra verification, and which fields it asks for, comes entirely from the\n * tenant's academy config / course data. This module never hardcodes a\n * sector-specific field name or label — the SDK does not know what a\n * \"colegiado\" is, and never will.\n *\n * Today the public course-detail contract (`AcademyCourseDetail`) exposes only\n * `requiresValidation` + `validationFieldLabel` — a single generic field, no\n * per-field `required` flag of its own (that flag lives on `AcademyConfig`,\n * server-side only, and isn't in the DTO yet). REB-83 is expected to add a\n * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,\n * label, required, maxLength? }]`) to that contract, which is how the upcoming\n * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code\n * change here.\n *\n * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from\n * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's\n * data actually has today, and remains satisfied once REB-83 lands a wider\n * real type — a course/config object with a `validationFields` array is already\n * assignable to this interface without a SITE-side type change.\n */\n\n/** A single tenant-defined (or derived) validation field the form should render. */\nexport interface AcademyValidationField {\n key: string\n label: string\n required: boolean\n maxLength?: number\n}\n\n/**\n * The shape a tenant-configured validation field list entry may take\n * (REB-83). `required` and `maxLength` are optional on the INPUT — the output\n * `AcademyValidationField` always carries a resolved `required`.\n */\nexport interface AcademyValidationFieldConfig {\n key: string\n label: string\n required?: boolean\n maxLength?: number\n}\n\n/**\n * Structural input — a subset of `AcademyCourseDetail` (today) plus the\n * optional REB-83 fields once the config/course exposes them. Any real DTO\n * that is at least this wide is assignable here without an import.\n */\nexport interface AcademyValidationSource {\n requiresValidation: boolean\n /** Present on `AcademyCourseDetail` today. */\n validationFieldLabel?: string | null\n /**\n * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —\n * accepted here so a SITE that already has it (e.g. from a wider tenant\n * config read) can pass it through; defaults to `true` (erring strict) when\n * absent, matching the existing demo-site precedent.\n */\n validationFieldRequired?: boolean | null\n /** REB-83 — tenant-defined list. When present and non-empty, takes priority\n * over the single-field fallback below. */\n validationFields?: AcademyValidationFieldConfig[] | null\n}\n\nexport interface DeriveValidationFieldsOptions {\n /** @default 'validationField' */\n genericKey?: string\n /** @default 'Dato de verificación' */\n genericLabel?: string\n}\n\nexport const DEFAULT_GENERIC_VALIDATION_KEY = 'validationField'\nexport const DEFAULT_GENERIC_VALIDATION_LABEL = 'Dato de verificación'\n\n/**\n * Derives the list of validation fields an enrollment form should render for\n * a course, from data alone.\n *\n * - `requiresValidation` is false → no fields (the form renders none).\n * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary\n * keys included, each entry's `required` defaulting to `true` when absent.\n * - Otherwise → falls back to the single generic field the SITE already knows\n * today, labeled from `validationFieldLabel` (or the generic fallback\n * label when the tenant configured no label).\n */\nexport function deriveValidationFields(\n source: AcademyValidationSource,\n options: DeriveValidationFieldsOptions = {},\n): AcademyValidationField[] {\n if (!source.requiresValidation) return []\n\n if (source.validationFields && source.validationFields.length > 0) {\n return source.validationFields.map((field) => ({\n key: field.key,\n label: field.label,\n required: field.required ?? true,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n }))\n }\n\n return [\n {\n key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,\n label:\n source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,\n required: source.validationFieldRequired ?? true,\n },\n ]\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2EO,IAAM,iCAAiC;AACvC,IAAM,mCAAmC;AAazC,SAAS,uBACd,QACA,UAAyC,CAAC,GAChB;AAC1B,MAAI,CAAC,OAAO,mBAAoB,QAAO,CAAC;AAExC,MAAI,OAAO,oBAAoB,OAAO,iBAAiB,SAAS,GAAG;AACjE,WAAO,OAAO,iBAAiB,IAAI,CAAC,WAAW;AAAA,MAC7C,KAAK,MAAM;AAAA,MACX,OAAO,MAAM;AAAA,MACb,UAAU,MAAM,YAAY;AAAA,MAC5B,GAAI,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IACxE,EAAE;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,MACE,KAAK,QAAQ,cAAc;AAAA,MAC3B,OACE,OAAO,wBAAwB,QAAQ,gBAAgB;AAAA,MACzD,UAAU,OAAO,2BAA2B;AAAA,IAC9C;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@reblu/site-next/academy` (also re-exported from the server-safe
|
|
3
|
+
* `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-
|
|
4
|
+
* field derivation (REB-584 follow-up).
|
|
5
|
+
*
|
|
6
|
+
* PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks
|
|
7
|
+
* for extra verification, and which fields it asks for, comes entirely from the
|
|
8
|
+
* tenant's academy config / course data. This module never hardcodes a
|
|
9
|
+
* sector-specific field name or label — the SDK does not know what a
|
|
10
|
+
* "colegiado" is, and never will.
|
|
11
|
+
*
|
|
12
|
+
* Today the public course-detail contract (`AcademyCourseDetail`) exposes only
|
|
13
|
+
* `requiresValidation` + `validationFieldLabel` — a single generic field, no
|
|
14
|
+
* per-field `required` flag of its own (that flag lives on `AcademyConfig`,
|
|
15
|
+
* server-side only, and isn't in the DTO yet). REB-83 is expected to add a
|
|
16
|
+
* genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,
|
|
17
|
+
* label, required, maxLength? }]`) to that contract, which is how the upcoming
|
|
18
|
+
* `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code
|
|
19
|
+
* change here.
|
|
20
|
+
*
|
|
21
|
+
* `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from
|
|
22
|
+
* `@reblu/site-contracts`: it accepts whatever subset of fields the caller's
|
|
23
|
+
* data actually has today, and remains satisfied once REB-83 lands a wider
|
|
24
|
+
* real type — a course/config object with a `validationFields` array is already
|
|
25
|
+
* assignable to this interface without a SITE-side type change.
|
|
26
|
+
*/
|
|
27
|
+
/** A single tenant-defined (or derived) validation field the form should render. */
|
|
28
|
+
interface AcademyValidationField {
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The shape a tenant-configured validation field list entry may take
|
|
36
|
+
* (REB-83). `required` and `maxLength` are optional on the INPUT — the output
|
|
37
|
+
* `AcademyValidationField` always carries a resolved `required`.
|
|
38
|
+
*/
|
|
39
|
+
interface AcademyValidationFieldConfig {
|
|
40
|
+
key: string;
|
|
41
|
+
label: string;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
maxLength?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Structural input — a subset of `AcademyCourseDetail` (today) plus the
|
|
47
|
+
* optional REB-83 fields once the config/course exposes them. Any real DTO
|
|
48
|
+
* that is at least this wide is assignable here without an import.
|
|
49
|
+
*/
|
|
50
|
+
interface AcademyValidationSource {
|
|
51
|
+
requiresValidation: boolean;
|
|
52
|
+
/** Present on `AcademyCourseDetail` today. */
|
|
53
|
+
validationFieldLabel?: string | null;
|
|
54
|
+
/**
|
|
55
|
+
* NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —
|
|
56
|
+
* accepted here so a SITE that already has it (e.g. from a wider tenant
|
|
57
|
+
* config read) can pass it through; defaults to `true` (erring strict) when
|
|
58
|
+
* absent, matching the existing demo-site precedent.
|
|
59
|
+
*/
|
|
60
|
+
validationFieldRequired?: boolean | null;
|
|
61
|
+
/** REB-83 — tenant-defined list. When present and non-empty, takes priority
|
|
62
|
+
* over the single-field fallback below. */
|
|
63
|
+
validationFields?: AcademyValidationFieldConfig[] | null;
|
|
64
|
+
}
|
|
65
|
+
interface DeriveValidationFieldsOptions {
|
|
66
|
+
/** @default 'validationField' */
|
|
67
|
+
genericKey?: string;
|
|
68
|
+
/** @default 'Dato de verificación' */
|
|
69
|
+
genericLabel?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derives the list of validation fields an enrollment form should render for
|
|
73
|
+
* a course, from data alone.
|
|
74
|
+
*
|
|
75
|
+
* - `requiresValidation` is false → no fields (the form renders none).
|
|
76
|
+
* - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary
|
|
77
|
+
* keys included, each entry's `required` defaulting to `true` when absent.
|
|
78
|
+
* - Otherwise → falls back to the single generic field the SITE already knows
|
|
79
|
+
* today, labeled from `validationFieldLabel` (or the generic fallback
|
|
80
|
+
* label when the tenant configured no label).
|
|
81
|
+
*/
|
|
82
|
+
declare function deriveValidationFields(source: AcademyValidationSource, options?: DeriveValidationFieldsOptions): AcademyValidationField[];
|
|
83
|
+
|
|
84
|
+
export { type AcademyValidationField, type AcademyValidationFieldConfig, type AcademyValidationSource, type DeriveValidationFieldsOptions, deriveValidationFields };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@reblu/site-next/academy` (also re-exported from the server-safe
|
|
3
|
+
* `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-
|
|
4
|
+
* field derivation (REB-584 follow-up).
|
|
5
|
+
*
|
|
6
|
+
* PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks
|
|
7
|
+
* for extra verification, and which fields it asks for, comes entirely from the
|
|
8
|
+
* tenant's academy config / course data. This module never hardcodes a
|
|
9
|
+
* sector-specific field name or label — the SDK does not know what a
|
|
10
|
+
* "colegiado" is, and never will.
|
|
11
|
+
*
|
|
12
|
+
* Today the public course-detail contract (`AcademyCourseDetail`) exposes only
|
|
13
|
+
* `requiresValidation` + `validationFieldLabel` — a single generic field, no
|
|
14
|
+
* per-field `required` flag of its own (that flag lives on `AcademyConfig`,
|
|
15
|
+
* server-side only, and isn't in the DTO yet). REB-83 is expected to add a
|
|
16
|
+
* genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,
|
|
17
|
+
* label, required, maxLength? }]`) to that contract, which is how the upcoming
|
|
18
|
+
* `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code
|
|
19
|
+
* change here.
|
|
20
|
+
*
|
|
21
|
+
* `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from
|
|
22
|
+
* `@reblu/site-contracts`: it accepts whatever subset of fields the caller's
|
|
23
|
+
* data actually has today, and remains satisfied once REB-83 lands a wider
|
|
24
|
+
* real type — a course/config object with a `validationFields` array is already
|
|
25
|
+
* assignable to this interface without a SITE-side type change.
|
|
26
|
+
*/
|
|
27
|
+
/** A single tenant-defined (or derived) validation field the form should render. */
|
|
28
|
+
interface AcademyValidationField {
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The shape a tenant-configured validation field list entry may take
|
|
36
|
+
* (REB-83). `required` and `maxLength` are optional on the INPUT — the output
|
|
37
|
+
* `AcademyValidationField` always carries a resolved `required`.
|
|
38
|
+
*/
|
|
39
|
+
interface AcademyValidationFieldConfig {
|
|
40
|
+
key: string;
|
|
41
|
+
label: string;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
maxLength?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Structural input — a subset of `AcademyCourseDetail` (today) plus the
|
|
47
|
+
* optional REB-83 fields once the config/course exposes them. Any real DTO
|
|
48
|
+
* that is at least this wide is assignable here without an import.
|
|
49
|
+
*/
|
|
50
|
+
interface AcademyValidationSource {
|
|
51
|
+
requiresValidation: boolean;
|
|
52
|
+
/** Present on `AcademyCourseDetail` today. */
|
|
53
|
+
validationFieldLabel?: string | null;
|
|
54
|
+
/**
|
|
55
|
+
* NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —
|
|
56
|
+
* accepted here so a SITE that already has it (e.g. from a wider tenant
|
|
57
|
+
* config read) can pass it through; defaults to `true` (erring strict) when
|
|
58
|
+
* absent, matching the existing demo-site precedent.
|
|
59
|
+
*/
|
|
60
|
+
validationFieldRequired?: boolean | null;
|
|
61
|
+
/** REB-83 — tenant-defined list. When present and non-empty, takes priority
|
|
62
|
+
* over the single-field fallback below. */
|
|
63
|
+
validationFields?: AcademyValidationFieldConfig[] | null;
|
|
64
|
+
}
|
|
65
|
+
interface DeriveValidationFieldsOptions {
|
|
66
|
+
/** @default 'validationField' */
|
|
67
|
+
genericKey?: string;
|
|
68
|
+
/** @default 'Dato de verificación' */
|
|
69
|
+
genericLabel?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derives the list of validation fields an enrollment form should render for
|
|
73
|
+
* a course, from data alone.
|
|
74
|
+
*
|
|
75
|
+
* - `requiresValidation` is false → no fields (the form renders none).
|
|
76
|
+
* - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary
|
|
77
|
+
* keys included, each entry's `required` defaulting to `true` when absent.
|
|
78
|
+
* - Otherwise → falls back to the single generic field the SITE already knows
|
|
79
|
+
* today, labeled from `validationFieldLabel` (or the generic fallback
|
|
80
|
+
* label when the tenant configured no label).
|
|
81
|
+
*/
|
|
82
|
+
declare function deriveValidationFields(source: AcademyValidationSource, options?: DeriveValidationFieldsOptions): AcademyValidationField[];
|
|
83
|
+
|
|
84
|
+
export { type AcademyValidationField, type AcademyValidationFieldConfig, type AcademyValidationSource, type DeriveValidationFieldsOptions, deriveValidationFields };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/academy/derive-validation-fields.ts
|
|
2
|
+
var DEFAULT_GENERIC_VALIDATION_KEY = "validationField";
|
|
3
|
+
var DEFAULT_GENERIC_VALIDATION_LABEL = "Dato de verificaci\xF3n";
|
|
4
|
+
function deriveValidationFields(source, options = {}) {
|
|
5
|
+
if (!source.requiresValidation) return [];
|
|
6
|
+
if (source.validationFields && source.validationFields.length > 0) {
|
|
7
|
+
return source.validationFields.map((field) => ({
|
|
8
|
+
key: field.key,
|
|
9
|
+
label: field.label,
|
|
10
|
+
required: field.required ?? true,
|
|
11
|
+
...field.maxLength !== void 0 ? { maxLength: field.maxLength } : {}
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,
|
|
17
|
+
label: source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,
|
|
18
|
+
required: source.validationFieldRequired ?? true
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
deriveValidationFields
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=academy-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/academy/derive-validation-fields.ts"],"sourcesContent":["/**\n * `@reblu/site-next/academy` (also re-exported from the server-safe\n * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-\n * field derivation (REB-584 follow-up).\n *\n * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks\n * for extra verification, and which fields it asks for, comes entirely from the\n * tenant's academy config / course data. This module never hardcodes a\n * sector-specific field name or label — the SDK does not know what a\n * \"colegiado\" is, and never will.\n *\n * Today the public course-detail contract (`AcademyCourseDetail`) exposes only\n * `requiresValidation` + `validationFieldLabel` — a single generic field, no\n * per-field `required` flag of its own (that flag lives on `AcademyConfig`,\n * server-side only, and isn't in the DTO yet). REB-83 is expected to add a\n * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,\n * label, required, maxLength? }]`) to that contract, which is how the upcoming\n * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code\n * change here.\n *\n * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from\n * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's\n * data actually has today, and remains satisfied once REB-83 lands a wider\n * real type — a course/config object with a `validationFields` array is already\n * assignable to this interface without a SITE-side type change.\n */\n\n/** A single tenant-defined (or derived) validation field the form should render. */\nexport interface AcademyValidationField {\n key: string\n label: string\n required: boolean\n maxLength?: number\n}\n\n/**\n * The shape a tenant-configured validation field list entry may take\n * (REB-83). `required` and `maxLength` are optional on the INPUT — the output\n * `AcademyValidationField` always carries a resolved `required`.\n */\nexport interface AcademyValidationFieldConfig {\n key: string\n label: string\n required?: boolean\n maxLength?: number\n}\n\n/**\n * Structural input — a subset of `AcademyCourseDetail` (today) plus the\n * optional REB-83 fields once the config/course exposes them. Any real DTO\n * that is at least this wide is assignable here without an import.\n */\nexport interface AcademyValidationSource {\n requiresValidation: boolean\n /** Present on `AcademyCourseDetail` today. */\n validationFieldLabel?: string | null\n /**\n * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —\n * accepted here so a SITE that already has it (e.g. from a wider tenant\n * config read) can pass it through; defaults to `true` (erring strict) when\n * absent, matching the existing demo-site precedent.\n */\n validationFieldRequired?: boolean | null\n /** REB-83 — tenant-defined list. When present and non-empty, takes priority\n * over the single-field fallback below. */\n validationFields?: AcademyValidationFieldConfig[] | null\n}\n\nexport interface DeriveValidationFieldsOptions {\n /** @default 'validationField' */\n genericKey?: string\n /** @default 'Dato de verificación' */\n genericLabel?: string\n}\n\nexport const DEFAULT_GENERIC_VALIDATION_KEY = 'validationField'\nexport const DEFAULT_GENERIC_VALIDATION_LABEL = 'Dato de verificación'\n\n/**\n * Derives the list of validation fields an enrollment form should render for\n * a course, from data alone.\n *\n * - `requiresValidation` is false → no fields (the form renders none).\n * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary\n * keys included, each entry's `required` defaulting to `true` when absent.\n * - Otherwise → falls back to the single generic field the SITE already knows\n * today, labeled from `validationFieldLabel` (or the generic fallback\n * label when the tenant configured no label).\n */\nexport function deriveValidationFields(\n source: AcademyValidationSource,\n options: DeriveValidationFieldsOptions = {},\n): AcademyValidationField[] {\n if (!source.requiresValidation) return []\n\n if (source.validationFields && source.validationFields.length > 0) {\n return source.validationFields.map((field) => ({\n key: field.key,\n label: field.label,\n required: field.required ?? true,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n }))\n }\n\n return [\n {\n key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,\n label:\n source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,\n required: source.validationFieldRequired ?? true,\n },\n ]\n}\n"],"mappings":";AA2EO,IAAM,iCAAiC;AACvC,IAAM,mCAAmC;AAazC,SAAS,uBACd,QACA,UAAyC,CAAC,GAChB;AAC1B,MAAI,CAAC,OAAO,mBAAoB,QAAO,CAAC;AAExC,MAAI,OAAO,oBAAoB,OAAO,iBAAiB,SAAS,GAAG;AACjE,WAAO,OAAO,iBAAiB,IAAI,CAAC,WAAW;AAAA,MAC7C,KAAK,MAAM;AAAA,MACX,OAAO,MAAM;AAAA,MACb,UAAU,MAAM,YAAY;AAAA,MAC5B,GAAI,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IACxE,EAAE;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,MACE,KAAK,QAAQ,cAAc;AAAA,MAC3B,OACE,OAAO,wBAAwB,QAAQ,gBAAgB;AAAA,MACzD,UAAU,OAAO,2BAA2B;AAAA,IAC9C;AAAA,EACF;AACF;","names":[]}
|
package/dist/academy.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/academy/index.ts","../src/academy/enrollment-form.tsx","../src/academy/derive-validation-fields.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/academy` — the academy enrollment-request form\n * (REB-584 follow-up).\n *\n * This barrel MUST begin with its OWN leading `'use client'` directive: tsup\n * only preserves a directive prologue declared by the entry file itself, not\n * one merely re-exported from a child module (same R4 rule as `./auth`, see\n * `tsup.config.ts`).\n *\n * `AcademyEnrollmentForm` was extracted from demo-site's `EnrollmentCta`\n * (#812, REB-584) and generalized to be data-driven per tenant —\n * `deriveValidationFields` is the pure helper a SITE uses to turn the course\n * detail its API already returned into the form's `validationFields` prop.\n */\nexport {\n AcademyEnrollmentForm,\n type AcademyEnrollmentFormProps,\n type AcademyEnrollmentFormCopy,\n} from './enrollment-form'\nexport {\n deriveValidationFields,\n type AcademyValidationField,\n type AcademyValidationFieldConfig,\n type AcademyValidationSource,\n type DeriveValidationFieldsOptions,\n} from './derive-validation-fields'\n","'use client'\n\nimport * as React from 'react'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\nimport type { AcademyValidationField } from './derive-validation-fields'\n\n/**\n * `AcademyEnrollmentForm` (REB-584 follow-up) — the SDK-published academy\n * enrollment-request form, extracted from demo-site's `EnrollmentCta` and\n * generalized to be PER-TENANT CUSTOMIZABLE and DATA-driven rather than\n * code-driven.\n *\n * Two things carried over from the original component are security/product\n * decisions, not styling ones — read `derive-validation-fields.ts`'s docblock\n * for the first, and this one for the second:\n *\n * The success/error copy this form shows is deliberately never \"your request\n * was created\" — the upstream route (`POST\n * /api/site/academy/enrollment-requests`) replies UNIFORMLY regardless of\n * whether the email was new (a request row was created) or already known (a\n * sign-in link was sent, no request row created), by anti-enumeration design.\n * `onSubmitted` fires on any 2xx, never implying the request was recorded.\n *\n * `validationFields` is fully DATA-driven (see `deriveValidationFields` in\n * this same subpath): when empty, this form renders no extra input at all and\n * submits with no `validationData` key — the negative control a\n * no-validation tenant needs. Arbitrary keys (REB-83's future\n * `collegiateNumber` / `professionalCollege`) work without a new release,\n * because this component never hardcodes a field name — it only ever\n * iterates the array it was given.\n */\n\nexport interface AcademyEnrollmentFormCopy {\n emailLabel: string\n emailPlaceholder: string\n /** Always visible — the request goes to human review, never an automatic purchase. */\n reviewNotice: string\n submitCta: string\n submittingCta: string\n successTitle: string\n /** Shown only when the server response carries no `message` of its own. */\n successFallbackMessage: string\n /** `{label}` is replaced with the field's own label (lowercased). */\n missingFieldError: string\n invalidEmailError: string\n rateLimitError: string\n networkError: string\n genericError: string\n}\n\nexport interface AcademyEnrollmentFormProps {\n /**\n * Where the form POSTs — a SITE-owned Route Handler (e.g. wired with\n * `createProxyRoute('/api/site/academy/enrollment-requests')` from\n * `@reblu/site-next/route-handlers`, or an equivalent bridge). The browser\n * can never call base-api directly: `X-Reblu-Api-Key` is a server secret.\n */\n enrollmentRequestUrl: string\n courseSlug: string\n /**\n * Data-driven validation fields for this course/tenant — build with\n * `deriveValidationFields` from the course/config data the SITE API\n * returned. Empty (the default) renders no extra field.\n */\n validationFields?: AcademyValidationField[]\n copy?: Partial<AcademyEnrollmentFormCopy>\n className?: string\n /** Fires on any 2xx response. Never treat this as confirmation a request was recorded — see module docblock. */\n onSubmitted?: (email: string) => void\n}\n\nconst DEFAULT_COPY: AcademyEnrollmentFormCopy = {\n emailLabel: 'Correo electrónico',\n emailPlaceholder: 'tu@email.com',\n reviewNotice:\n 'Tu solicitud será revisada por nuestro equipo antes de confirmarse. No se realiza ningún cobro automático: si es aprobada, recibirás un email con un enlace para completar el pago.',\n submitCta: 'Solicitar inscripción',\n submittingCta: 'Enviando...',\n successTitle: 'Solicitud enviada',\n successFallbackMessage: 'Si tu solicitud es válida, recibirás un email con los siguientes pasos.',\n missingFieldError: 'Introduce {label}.',\n invalidEmailError: 'Introduce un correo electrónico válido.',\n rateLimitError: 'Demasiadas solicitudes. Espera unos minutos antes de volver a intentarlo.',\n networkError: 'Error de conexión. Por favor, inténtalo de nuevo.',\n genericError: 'No hemos podido procesar la solicitud. Inténtalo de nuevo.',\n}\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value)\n}\n\ninterface ServerErrorBody {\n error?: string\n message?: string\n // Some SITE-owned bridges unwrap upstream into `{ data: { message } }` on\n // success and pass `error` through as a plain string on failure — this form\n // is written to work behind either shape (see module docblock).\n data?: { message?: string }\n details?: { fieldErrors?: Record<string, string[]>; formErrors?: string[] }\n}\n\nasync function parseJsonSafely(res: Response): Promise<ServerErrorBody> {\n try {\n return (await res.json()) as ServerErrorBody\n } catch {\n return {}\n }\n}\n\ntype FormState = 'idle' | 'submitting' | 'done' | 'error'\n\nexport function AcademyEnrollmentForm(props: AcademyEnrollmentFormProps) {\n const {\n enrollmentRequestUrl,\n courseSlug,\n validationFields = [],\n copy,\n className,\n onSubmitted,\n } = props\n const c = { ...DEFAULT_COPY, ...copy }\n\n const [email, setEmail] = React.useState('')\n const [values, setValues] = React.useState<Record<string, string>>({})\n const [state, setState] = React.useState<FormState>('idle')\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const [generalError, setGeneralError] = React.useState<string | null>(null)\n const [successMessage, setSuccessMessage] = React.useState<string>('')\n\n const emailRef = React.useRef<HTMLInputElement>(null)\n const fieldRefs = React.useRef(new Map<string, HTMLInputElement>())\n const generalErrorRef = React.useRef<HTMLParagraphElement>(null)\n const successHeadingRef = React.useRef<HTMLHeadingElement>(null)\n\n // On success the form is replaced by the result panel, so the button that\n // was just pressed unmounts and focus would fall to <body> — dropping a\n // keyboard/screen-reader user at the top of the document. Move it instead.\n React.useEffect(() => {\n if (state === 'done') successHeadingRef.current?.focus()\n }, [state])\n\n // On an error, move focus to the FIRST thing actually wrong, in DOM order:\n // the email field, then the first validation field with an error, then the\n // form-level error paragraph. Ties the announcement to what it is about\n // (WCAG 3.3.1) instead of leaving a keyboard user to hunt for it.\n React.useEffect(() => {\n if (state !== 'error') return\n if (fieldErrors.email) {\n emailRef.current?.focus()\n return\n }\n const firstInvalidField = validationFields.find((field) => fieldErrors[field.key])\n if (firstInvalidField) {\n fieldRefs.current.get(firstInvalidField.key)?.focus()\n return\n }\n generalErrorRef.current?.focus()\n // `validationFields` is a per-render prop, not state this effect reacts\n // to — it only needs the CURRENT list at the moment `state`/`fieldErrors`\n // change, never a re-run when the (structurally stable, caller-owned)\n // array reference changes on its own.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state, fieldErrors])\n\n function setFieldValue(key: string, value: string) {\n setValues((prev) => ({ ...prev, [key]: value }))\n }\n\n function validateClientSide(trimmedEmail: string): Record<string, string> {\n const errors: Record<string, string> = {}\n if (!isValidEmail(trimmedEmail)) {\n errors.email = c.invalidEmailError\n }\n for (const field of validationFields) {\n if (!field.required) continue\n const value = (values[field.key] ?? '').trim()\n if (!value) {\n errors[field.key] = c.missingFieldError.replace('{label}', field.label.toLowerCase())\n }\n }\n return errors\n }\n\n function applyServerFieldErrors(details: ServerErrorBody['details']): {\n matched: Record<string, string>\n leftover: string[]\n } {\n const matched: Record<string, string> = {}\n const leftover: string[] = []\n const known = new Set(['email', ...validationFields.map((f) => f.key)])\n\n for (const [key, messages] of Object.entries(details?.fieldErrors ?? {})) {\n const message = messages?.[0]\n if (!message) continue\n if (known.has(key)) {\n matched[key] = message\n } else {\n leftover.push(message)\n }\n }\n return { matched, leftover }\n }\n\n async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault()\n if (state === 'submitting') return\n\n setGeneralError(null)\n setFieldErrors({})\n\n const trimmedEmail = email.trim().toLowerCase()\n const clientErrors = validateClientSide(trimmedEmail)\n if (Object.keys(clientErrors).length > 0) {\n setFieldErrors(clientErrors)\n setState('error')\n return\n }\n\n setState('submitting')\n\n let succeeded = false\n let messageToReport = ''\n try {\n const res = await fetch(enrollmentRequestUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n courseSlug,\n email: trimmedEmail,\n ...(validationFields.length > 0\n ? {\n validationData: Object.fromEntries(\n validationFields.map((field) => [field.key, (values[field.key] ?? '').trim()]),\n ),\n }\n : {}),\n }),\n })\n\n if (res.status === 429) {\n setGeneralError(c.rateLimitError)\n setState('error')\n return\n }\n\n const json = await parseJsonSafely(res)\n\n if (!res.ok) {\n const { matched, leftover } = applyServerFieldErrors(json.details)\n if (Object.keys(matched).length > 0) {\n setFieldErrors(matched)\n if (leftover.length > 0) setGeneralError(leftover.join(' '))\n setState('error')\n return\n }\n const fallback =\n json.message ||\n (typeof json.error === 'string' ? json.error : undefined) ||\n leftover.join(' ') ||\n c.genericError\n setGeneralError(fallback)\n setState('error')\n return\n }\n\n messageToReport = json.data?.message || json.message || c.successFallbackMessage\n succeeded = true\n } catch {\n setGeneralError(c.networkError)\n setState('error')\n return\n }\n\n if (succeeded) {\n setSuccessMessage(messageToReport)\n setState('done')\n try {\n onSubmitted?.(trimmedEmail)\n } catch (err) {\n console.error('[AcademyEnrollmentForm] onSubmitted callback threw:', err)\n }\n }\n }\n\n const reviewNotice = <p className='text-muted-foreground text-sm'>{c.reviewNotice}</p>\n\n if (state === 'done') {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n {reviewNotice}\n <div className='space-y-1.5 rounded-xl border border-border p-5'>\n <h2 className='text-lg font-semibold' ref={successHeadingRef} tabIndex={-1}>\n {c.successTitle}\n </h2>\n <p className='text-muted-foreground text-sm'>{successMessage}</p>\n </div>\n </div>\n )\n }\n\n const submitting = state === 'submitting'\n\n return (\n <div className={cn('space-y-4', className)}>\n {reviewNotice}\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className='space-y-4 rounded-xl border border-border p-5'\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='academy-enrollment-email'>{c.emailLabel}</Label>\n <Input\n id='academy-enrollment-email'\n ref={emailRef}\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={submitting}\n aria-invalid={fieldErrors.email ? true : undefined}\n aria-describedby={fieldErrors.email ? 'academy-enrollment-email-error' : undefined}\n />\n {fieldErrors.email && (\n <p\n id='academy-enrollment-email-error'\n role='alert'\n className='text-destructive text-sm'\n >\n {fieldErrors.email}\n </p>\n )}\n </div>\n\n {validationFields.map((field) => {\n const inputId = `academy-enrollment-${field.key}`\n const errorId = `${inputId}-error`\n const fieldError = fieldErrors[field.key]\n return (\n <div className='space-y-1.5' key={field.key}>\n <Label htmlFor={inputId}>{field.label}</Label>\n <Input\n id={inputId}\n ref={(el) => {\n if (el) fieldRefs.current.set(field.key, el)\n else fieldRefs.current.delete(field.key)\n }}\n type='text'\n maxLength={field.maxLength}\n value={values[field.key] ?? ''}\n onChange={(e) => setFieldValue(field.key, e.target.value)}\n disabled={submitting}\n aria-invalid={fieldError ? true : undefined}\n aria-describedby={fieldError ? errorId : undefined}\n />\n {fieldError && (\n <p id={errorId} role='alert' className='text-destructive text-sm'>\n {fieldError}\n </p>\n )}\n </div>\n )\n })}\n\n {generalError && (\n <p\n ref={generalErrorRef}\n role='alert'\n tabIndex={-1}\n className='text-destructive text-sm'\n >\n {generalError}\n </p>\n )}\n\n <Button type='submit' className='w-full' disabled={submitting}>\n {submitting ? c.submittingCta : c.submitCta}\n </Button>\n </form>\n </div>\n )\n}\n","/**\n * `@reblu/site-next/academy` — validation-field derivation (REB-584 follow-up).\n *\n * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks\n * for extra verification, and which fields it asks for, comes entirely from the\n * tenant's academy config / course data. This module never hardcodes a\n * sector-specific field name or label — the SDK does not know what a\n * \"colegiado\" is, and never will.\n *\n * Today the public course-detail contract (`AcademyCourseDetail`) exposes only\n * `requiresValidation` + `validationFieldLabel` — a single generic field, no\n * per-field `required` flag of its own (that flag lives on `AcademyConfig`,\n * server-side only, and isn't in the DTO yet). REB-83 is expected to add a\n * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,\n * label, required, maxLength? }]`) to that contract, which is how the upcoming\n * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code\n * change here.\n *\n * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from\n * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's\n * data actually has today, and remains satisfied once REB-83 lands a wider\n * real type — a course/config object with a `validationFields` array is already\n * assignable to this interface without a SITE-side type change.\n */\n\n/** A single tenant-defined (or derived) validation field the form should render. */\nexport interface AcademyValidationField {\n key: string\n label: string\n required: boolean\n maxLength?: number\n}\n\n/**\n * The shape a tenant-configured validation field list entry may take\n * (REB-83). `required` and `maxLength` are optional on the INPUT — the output\n * `AcademyValidationField` always carries a resolved `required`.\n */\nexport interface AcademyValidationFieldConfig {\n key: string\n label: string\n required?: boolean\n maxLength?: number\n}\n\n/**\n * Structural input — a subset of `AcademyCourseDetail` (today) plus the\n * optional REB-83 fields once the config/course exposes them. Any real DTO\n * that is at least this wide is assignable here without an import.\n */\nexport interface AcademyValidationSource {\n requiresValidation: boolean\n /** Present on `AcademyCourseDetail` today. */\n validationFieldLabel?: string | null\n /**\n * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —\n * accepted here so a SITE that already has it (e.g. from a wider tenant\n * config read) can pass it through; defaults to `true` (erring strict) when\n * absent, matching the existing demo-site precedent.\n */\n validationFieldRequired?: boolean | null\n /** REB-83 — tenant-defined list. When present and non-empty, takes priority\n * over the single-field fallback below. */\n validationFields?: AcademyValidationFieldConfig[] | null\n}\n\nexport interface DeriveValidationFieldsOptions {\n /** @default 'validationField' */\n genericKey?: string\n /** @default 'Dato de verificación' */\n genericLabel?: string\n}\n\nexport const DEFAULT_GENERIC_VALIDATION_KEY = 'validationField'\nexport const DEFAULT_GENERIC_VALIDATION_LABEL = 'Dato de verificación'\n\n/**\n * Derives the list of validation fields an enrollment form should render for\n * a course, from data alone.\n *\n * - `requiresValidation` is false → no fields (the form renders none).\n * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary\n * keys included, each entry's `required` defaulting to `true` when absent.\n * - Otherwise → falls back to the single generic field the SITE already knows\n * today, labeled from `validationFieldLabel` (or the generic fallback\n * label when the tenant configured no label).\n */\nexport function deriveValidationFields(\n source: AcademyValidationSource,\n options: DeriveValidationFieldsOptions = {},\n): AcademyValidationField[] {\n if (!source.requiresValidation) return []\n\n if (source.validationFields && source.validationFields.length > 0) {\n return source.validationFields.map((field) => ({\n key: field.key,\n label: field.label,\n required: field.required ?? true,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n }))\n }\n\n return [\n {\n key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,\n label:\n source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,\n required: source.validationFieldRequired ?? true,\n },\n ]\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/academy/index.ts","../src/academy/enrollment-form.tsx","../src/academy/derive-validation-fields.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/academy` — the academy enrollment-request form\n * (REB-584 follow-up).\n *\n * This barrel MUST begin with its OWN leading `'use client'` directive: tsup\n * only preserves a directive prologue declared by the entry file itself, not\n * one merely re-exported from a child module (same R4 rule as `./auth`, see\n * `tsup.config.ts`).\n *\n * `AcademyEnrollmentForm` was extracted from demo-site's `EnrollmentCta`\n * (#812, REB-584) and generalized to be data-driven per tenant —\n * `deriveValidationFields` is the pure helper a SITE uses to turn the course\n * detail its API already returned into the form's `validationFields` prop.\n */\nexport {\n AcademyEnrollmentForm,\n type AcademyEnrollmentFormProps,\n type AcademyEnrollmentFormCopy,\n} from './enrollment-form'\nexport {\n deriveValidationFields,\n type AcademyValidationField,\n type AcademyValidationFieldConfig,\n type AcademyValidationSource,\n type DeriveValidationFieldsOptions,\n} from './derive-validation-fields'\n","'use client'\n\nimport * as React from 'react'\nimport { Button, Input, cn } from '@reblu/site-ui'\nimport { Label } from '@reblu/site-ui/label'\nimport type { AcademyValidationField } from './derive-validation-fields'\n\n/**\n * `AcademyEnrollmentForm` (REB-584 follow-up) — the SDK-published academy\n * enrollment-request form, extracted from demo-site's `EnrollmentCta` and\n * generalized to be PER-TENANT CUSTOMIZABLE and DATA-driven rather than\n * code-driven.\n *\n * Two things carried over from the original component are security/product\n * decisions, not styling ones — read `derive-validation-fields.ts`'s docblock\n * for the first, and this one for the second:\n *\n * The success/error copy this form shows is deliberately never \"your request\n * was created\" — the upstream route (`POST\n * /api/site/academy/enrollment-requests`) replies UNIFORMLY regardless of\n * whether the email was new (a request row was created) or already known (a\n * sign-in link was sent, no request row created), by anti-enumeration design.\n * `onSubmitted` fires on any 2xx, never implying the request was recorded.\n *\n * `validationFields` is fully DATA-driven (see `deriveValidationFields` in\n * this same subpath): when empty, this form renders no extra input at all and\n * submits with no `validationData` key — the negative control a\n * no-validation tenant needs. Arbitrary keys (REB-83's future\n * `collegiateNumber` / `professionalCollege`) work without a new release,\n * because this component never hardcodes a field name — it only ever\n * iterates the array it was given.\n */\n\nexport interface AcademyEnrollmentFormCopy {\n emailLabel: string\n emailPlaceholder: string\n /** Always visible — the request goes to human review, never an automatic purchase. */\n reviewNotice: string\n submitCta: string\n submittingCta: string\n successTitle: string\n /** Shown only when the server response carries no `message` of its own. */\n successFallbackMessage: string\n /** `{label}` is replaced with the field's own label (lowercased). */\n missingFieldError: string\n invalidEmailError: string\n rateLimitError: string\n networkError: string\n genericError: string\n}\n\nexport interface AcademyEnrollmentFormProps {\n /**\n * Where the form POSTs — a SITE-owned Route Handler (e.g. wired with\n * `createProxyRoute('/api/site/academy/enrollment-requests')` from\n * `@reblu/site-next/route-handlers`, or an equivalent bridge). The browser\n * can never call base-api directly: `X-Reblu-Api-Key` is a server secret.\n */\n enrollmentRequestUrl: string\n courseSlug: string\n /**\n * Data-driven validation fields for this course/tenant — build with\n * `deriveValidationFields` from the course/config data the SITE API\n * returned. Empty (the default) renders no extra field.\n */\n validationFields?: AcademyValidationField[]\n copy?: Partial<AcademyEnrollmentFormCopy>\n className?: string\n /** Fires on any 2xx response. Never treat this as confirmation a request was recorded — see module docblock. */\n onSubmitted?: (email: string) => void\n}\n\nconst DEFAULT_COPY: AcademyEnrollmentFormCopy = {\n emailLabel: 'Correo electrónico',\n emailPlaceholder: 'tu@email.com',\n reviewNotice:\n 'Tu solicitud será revisada por nuestro equipo antes de confirmarse. No se realiza ningún cobro automático: si es aprobada, recibirás un email con un enlace para completar el pago.',\n submitCta: 'Solicitar inscripción',\n submittingCta: 'Enviando...',\n successTitle: 'Solicitud enviada',\n successFallbackMessage: 'Si tu solicitud es válida, recibirás un email con los siguientes pasos.',\n missingFieldError: 'Introduce {label}.',\n invalidEmailError: 'Introduce un correo electrónico válido.',\n rateLimitError: 'Demasiadas solicitudes. Espera unos minutos antes de volver a intentarlo.',\n networkError: 'Error de conexión. Por favor, inténtalo de nuevo.',\n genericError: 'No hemos podido procesar la solicitud. Inténtalo de nuevo.',\n}\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value)\n}\n\ninterface ServerErrorBody {\n error?: string\n message?: string\n // Some SITE-owned bridges unwrap upstream into `{ data: { message } }` on\n // success and pass `error` through as a plain string on failure — this form\n // is written to work behind either shape (see module docblock).\n data?: { message?: string }\n details?: { fieldErrors?: Record<string, string[]>; formErrors?: string[] }\n}\n\nasync function parseJsonSafely(res: Response): Promise<ServerErrorBody> {\n try {\n return (await res.json()) as ServerErrorBody\n } catch {\n return {}\n }\n}\n\ntype FormState = 'idle' | 'submitting' | 'done' | 'error'\n\nexport function AcademyEnrollmentForm(props: AcademyEnrollmentFormProps) {\n const {\n enrollmentRequestUrl,\n courseSlug,\n validationFields = [],\n copy,\n className,\n onSubmitted,\n } = props\n const c = { ...DEFAULT_COPY, ...copy }\n\n const [email, setEmail] = React.useState('')\n const [values, setValues] = React.useState<Record<string, string>>({})\n const [state, setState] = React.useState<FormState>('idle')\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const [generalError, setGeneralError] = React.useState<string | null>(null)\n const [successMessage, setSuccessMessage] = React.useState<string>('')\n\n const emailRef = React.useRef<HTMLInputElement>(null)\n const fieldRefs = React.useRef(new Map<string, HTMLInputElement>())\n const generalErrorRef = React.useRef<HTMLParagraphElement>(null)\n const successHeadingRef = React.useRef<HTMLHeadingElement>(null)\n\n // On success the form is replaced by the result panel, so the button that\n // was just pressed unmounts and focus would fall to <body> — dropping a\n // keyboard/screen-reader user at the top of the document. Move it instead.\n React.useEffect(() => {\n if (state === 'done') successHeadingRef.current?.focus()\n }, [state])\n\n // On an error, move focus to the FIRST thing actually wrong, in DOM order:\n // the email field, then the first validation field with an error, then the\n // form-level error paragraph. Ties the announcement to what it is about\n // (WCAG 3.3.1) instead of leaving a keyboard user to hunt for it.\n React.useEffect(() => {\n if (state !== 'error') return\n if (fieldErrors.email) {\n emailRef.current?.focus()\n return\n }\n const firstInvalidField = validationFields.find((field) => fieldErrors[field.key])\n if (firstInvalidField) {\n fieldRefs.current.get(firstInvalidField.key)?.focus()\n return\n }\n generalErrorRef.current?.focus()\n // `validationFields` is a per-render prop, not state this effect reacts\n // to — it only needs the CURRENT list at the moment `state`/`fieldErrors`\n // change, never a re-run when the (structurally stable, caller-owned)\n // array reference changes on its own.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state, fieldErrors])\n\n function setFieldValue(key: string, value: string) {\n setValues((prev) => ({ ...prev, [key]: value }))\n }\n\n function validateClientSide(trimmedEmail: string): Record<string, string> {\n const errors: Record<string, string> = {}\n if (!isValidEmail(trimmedEmail)) {\n errors.email = c.invalidEmailError\n }\n for (const field of validationFields) {\n if (!field.required) continue\n const value = (values[field.key] ?? '').trim()\n if (!value) {\n errors[field.key] = c.missingFieldError.replace('{label}', field.label.toLowerCase())\n }\n }\n return errors\n }\n\n function applyServerFieldErrors(details: ServerErrorBody['details']): {\n matched: Record<string, string>\n leftover: string[]\n } {\n const matched: Record<string, string> = {}\n const leftover: string[] = []\n const known = new Set(['email', ...validationFields.map((f) => f.key)])\n\n for (const [key, messages] of Object.entries(details?.fieldErrors ?? {})) {\n const message = messages?.[0]\n if (!message) continue\n if (known.has(key)) {\n matched[key] = message\n } else {\n leftover.push(message)\n }\n }\n return { matched, leftover }\n }\n\n async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n event.preventDefault()\n if (state === 'submitting') return\n\n setGeneralError(null)\n setFieldErrors({})\n\n const trimmedEmail = email.trim().toLowerCase()\n const clientErrors = validateClientSide(trimmedEmail)\n if (Object.keys(clientErrors).length > 0) {\n setFieldErrors(clientErrors)\n setState('error')\n return\n }\n\n setState('submitting')\n\n let succeeded = false\n let messageToReport = ''\n try {\n const res = await fetch(enrollmentRequestUrl, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n courseSlug,\n email: trimmedEmail,\n ...(validationFields.length > 0\n ? {\n validationData: Object.fromEntries(\n validationFields.map((field) => [field.key, (values[field.key] ?? '').trim()]),\n ),\n }\n : {}),\n }),\n })\n\n if (res.status === 429) {\n setGeneralError(c.rateLimitError)\n setState('error')\n return\n }\n\n const json = await parseJsonSafely(res)\n\n if (!res.ok) {\n const { matched, leftover } = applyServerFieldErrors(json.details)\n if (Object.keys(matched).length > 0) {\n setFieldErrors(matched)\n if (leftover.length > 0) setGeneralError(leftover.join(' '))\n setState('error')\n return\n }\n const fallback =\n json.message ||\n (typeof json.error === 'string' ? json.error : undefined) ||\n leftover.join(' ') ||\n c.genericError\n setGeneralError(fallback)\n setState('error')\n return\n }\n\n messageToReport = json.data?.message || json.message || c.successFallbackMessage\n succeeded = true\n } catch {\n setGeneralError(c.networkError)\n setState('error')\n return\n }\n\n if (succeeded) {\n setSuccessMessage(messageToReport)\n setState('done')\n try {\n onSubmitted?.(trimmedEmail)\n } catch (err) {\n console.error('[AcademyEnrollmentForm] onSubmitted callback threw:', err)\n }\n }\n }\n\n const reviewNotice = <p className='text-muted-foreground text-sm'>{c.reviewNotice}</p>\n\n if (state === 'done') {\n return (\n <div className={cn('space-y-4 text-center', className)}>\n {reviewNotice}\n <div className='space-y-1.5 rounded-xl border border-border p-5'>\n <h2 className='text-lg font-semibold' ref={successHeadingRef} tabIndex={-1}>\n {c.successTitle}\n </h2>\n <p className='text-muted-foreground text-sm'>{successMessage}</p>\n </div>\n </div>\n )\n }\n\n const submitting = state === 'submitting'\n\n return (\n <div className={cn('space-y-4', className)}>\n {reviewNotice}\n <form\n onSubmit={(e) => void handleSubmit(e)}\n className='space-y-4 rounded-xl border border-border p-5'\n noValidate\n >\n <div className='space-y-1.5'>\n <Label htmlFor='academy-enrollment-email'>{c.emailLabel}</Label>\n <Input\n id='academy-enrollment-email'\n ref={emailRef}\n type='email'\n autoComplete='email'\n placeholder={c.emailPlaceholder}\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={submitting}\n aria-invalid={fieldErrors.email ? true : undefined}\n aria-describedby={fieldErrors.email ? 'academy-enrollment-email-error' : undefined}\n />\n {fieldErrors.email && (\n <p\n id='academy-enrollment-email-error'\n role='alert'\n className='text-destructive text-sm'\n >\n {fieldErrors.email}\n </p>\n )}\n </div>\n\n {validationFields.map((field) => {\n const inputId = `academy-enrollment-${field.key}`\n const errorId = `${inputId}-error`\n const fieldError = fieldErrors[field.key]\n return (\n <div className='space-y-1.5' key={field.key}>\n <Label htmlFor={inputId}>{field.label}</Label>\n <Input\n id={inputId}\n ref={(el) => {\n if (el) fieldRefs.current.set(field.key, el)\n else fieldRefs.current.delete(field.key)\n }}\n type='text'\n maxLength={field.maxLength}\n value={values[field.key] ?? ''}\n onChange={(e) => setFieldValue(field.key, e.target.value)}\n disabled={submitting}\n aria-invalid={fieldError ? true : undefined}\n aria-describedby={fieldError ? errorId : undefined}\n />\n {fieldError && (\n <p id={errorId} role='alert' className='text-destructive text-sm'>\n {fieldError}\n </p>\n )}\n </div>\n )\n })}\n\n {generalError && (\n <p\n ref={generalErrorRef}\n role='alert'\n tabIndex={-1}\n className='text-destructive text-sm'\n >\n {generalError}\n </p>\n )}\n\n <Button type='submit' className='w-full' disabled={submitting}>\n {submitting ? c.submittingCta : c.submitCta}\n </Button>\n </form>\n </div>\n )\n}\n","/**\n * `@reblu/site-next/academy` (also re-exported from the server-safe\n * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-\n * field derivation (REB-584 follow-up).\n *\n * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks\n * for extra verification, and which fields it asks for, comes entirely from the\n * tenant's academy config / course data. This module never hardcodes a\n * sector-specific field name or label — the SDK does not know what a\n * \"colegiado\" is, and never will.\n *\n * Today the public course-detail contract (`AcademyCourseDetail`) exposes only\n * `requiresValidation` + `validationFieldLabel` — a single generic field, no\n * per-field `required` flag of its own (that flag lives on `AcademyConfig`,\n * server-side only, and isn't in the DTO yet). REB-83 is expected to add a\n * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,\n * label, required, maxLength? }]`) to that contract, which is how the upcoming\n * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code\n * change here.\n *\n * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from\n * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's\n * data actually has today, and remains satisfied once REB-83 lands a wider\n * real type — a course/config object with a `validationFields` array is already\n * assignable to this interface without a SITE-side type change.\n */\n\n/** A single tenant-defined (or derived) validation field the form should render. */\nexport interface AcademyValidationField {\n key: string\n label: string\n required: boolean\n maxLength?: number\n}\n\n/**\n * The shape a tenant-configured validation field list entry may take\n * (REB-83). `required` and `maxLength` are optional on the INPUT — the output\n * `AcademyValidationField` always carries a resolved `required`.\n */\nexport interface AcademyValidationFieldConfig {\n key: string\n label: string\n required?: boolean\n maxLength?: number\n}\n\n/**\n * Structural input — a subset of `AcademyCourseDetail` (today) plus the\n * optional REB-83 fields once the config/course exposes them. Any real DTO\n * that is at least this wide is assignable here without an import.\n */\nexport interface AcademyValidationSource {\n requiresValidation: boolean\n /** Present on `AcademyCourseDetail` today. */\n validationFieldLabel?: string | null\n /**\n * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —\n * accepted here so a SITE that already has it (e.g. from a wider tenant\n * config read) can pass it through; defaults to `true` (erring strict) when\n * absent, matching the existing demo-site precedent.\n */\n validationFieldRequired?: boolean | null\n /** REB-83 — tenant-defined list. When present and non-empty, takes priority\n * over the single-field fallback below. */\n validationFields?: AcademyValidationFieldConfig[] | null\n}\n\nexport interface DeriveValidationFieldsOptions {\n /** @default 'validationField' */\n genericKey?: string\n /** @default 'Dato de verificación' */\n genericLabel?: string\n}\n\nexport const DEFAULT_GENERIC_VALIDATION_KEY = 'validationField'\nexport const DEFAULT_GENERIC_VALIDATION_LABEL = 'Dato de verificación'\n\n/**\n * Derives the list of validation fields an enrollment form should render for\n * a course, from data alone.\n *\n * - `requiresValidation` is false → no fields (the form renders none).\n * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary\n * keys included, each entry's `required` defaulting to `true` when absent.\n * - Otherwise → falls back to the single generic field the SITE already knows\n * today, labeled from `validationFieldLabel` (or the generic fallback\n * label when the tenant configured no label).\n */\nexport function deriveValidationFields(\n source: AcademyValidationSource,\n options: DeriveValidationFieldsOptions = {},\n): AcademyValidationField[] {\n if (!source.requiresValidation) return []\n\n if (source.validationFields && source.validationFields.length > 0) {\n return source.validationFields.map((field) => ({\n key: field.key,\n label: field.label,\n required: field.required ?? true,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n }))\n }\n\n return [\n {\n key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,\n label:\n source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,\n required: source.validationFieldRequired ?? true,\n },\n ]\n}\n"],"mappings":"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","names":[]}
|
package/dist/academy.d.cts
CHANGED
|
@@ -1,85 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* `@reblu/site-next/academy` — validation-field derivation (REB-584 follow-up).
|
|
5
|
-
*
|
|
6
|
-
* PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks
|
|
7
|
-
* for extra verification, and which fields it asks for, comes entirely from the
|
|
8
|
-
* tenant's academy config / course data. This module never hardcodes a
|
|
9
|
-
* sector-specific field name or label — the SDK does not know what a
|
|
10
|
-
* "colegiado" is, and never will.
|
|
11
|
-
*
|
|
12
|
-
* Today the public course-detail contract (`AcademyCourseDetail`) exposes only
|
|
13
|
-
* `requiresValidation` + `validationFieldLabel` — a single generic field, no
|
|
14
|
-
* per-field `required` flag of its own (that flag lives on `AcademyConfig`,
|
|
15
|
-
* server-side only, and isn't in the DTO yet). REB-83 is expected to add a
|
|
16
|
-
* genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,
|
|
17
|
-
* label, required, maxLength? }]`) to that contract, which is how the upcoming
|
|
18
|
-
* `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code
|
|
19
|
-
* change here.
|
|
20
|
-
*
|
|
21
|
-
* `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from
|
|
22
|
-
* `@reblu/site-contracts`: it accepts whatever subset of fields the caller's
|
|
23
|
-
* data actually has today, and remains satisfied once REB-83 lands a wider
|
|
24
|
-
* real type — a course/config object with a `validationFields` array is already
|
|
25
|
-
* assignable to this interface without a SITE-side type change.
|
|
26
|
-
*/
|
|
27
|
-
/** A single tenant-defined (or derived) validation field the form should render. */
|
|
28
|
-
interface AcademyValidationField {
|
|
29
|
-
key: string;
|
|
30
|
-
label: string;
|
|
31
|
-
required: boolean;
|
|
32
|
-
maxLength?: number;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The shape a tenant-configured validation field list entry may take
|
|
36
|
-
* (REB-83). `required` and `maxLength` are optional on the INPUT — the output
|
|
37
|
-
* `AcademyValidationField` always carries a resolved `required`.
|
|
38
|
-
*/
|
|
39
|
-
interface AcademyValidationFieldConfig {
|
|
40
|
-
key: string;
|
|
41
|
-
label: string;
|
|
42
|
-
required?: boolean;
|
|
43
|
-
maxLength?: number;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Structural input — a subset of `AcademyCourseDetail` (today) plus the
|
|
47
|
-
* optional REB-83 fields once the config/course exposes them. Any real DTO
|
|
48
|
-
* that is at least this wide is assignable here without an import.
|
|
49
|
-
*/
|
|
50
|
-
interface AcademyValidationSource {
|
|
51
|
-
requiresValidation: boolean;
|
|
52
|
-
/** Present on `AcademyCourseDetail` today. */
|
|
53
|
-
validationFieldLabel?: string | null;
|
|
54
|
-
/**
|
|
55
|
-
* NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —
|
|
56
|
-
* accepted here so a SITE that already has it (e.g. from a wider tenant
|
|
57
|
-
* config read) can pass it through; defaults to `true` (erring strict) when
|
|
58
|
-
* absent, matching the existing demo-site precedent.
|
|
59
|
-
*/
|
|
60
|
-
validationFieldRequired?: boolean | null;
|
|
61
|
-
/** REB-83 — tenant-defined list. When present and non-empty, takes priority
|
|
62
|
-
* over the single-field fallback below. */
|
|
63
|
-
validationFields?: AcademyValidationFieldConfig[] | null;
|
|
64
|
-
}
|
|
65
|
-
interface DeriveValidationFieldsOptions {
|
|
66
|
-
/** @default 'validationField' */
|
|
67
|
-
genericKey?: string;
|
|
68
|
-
/** @default 'Dato de verificación' */
|
|
69
|
-
genericLabel?: string;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Derives the list of validation fields an enrollment form should render for
|
|
73
|
-
* a course, from data alone.
|
|
74
|
-
*
|
|
75
|
-
* - `requiresValidation` is false → no fields (the form renders none).
|
|
76
|
-
* - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary
|
|
77
|
-
* keys included, each entry's `required` defaulting to `true` when absent.
|
|
78
|
-
* - Otherwise → falls back to the single generic field the SITE already knows
|
|
79
|
-
* today, labeled from `validationFieldLabel` (or the generic fallback
|
|
80
|
-
* label when the tenant configured no label).
|
|
81
|
-
*/
|
|
82
|
-
declare function deriveValidationFields(source: AcademyValidationSource, options?: DeriveValidationFieldsOptions): AcademyValidationField[];
|
|
2
|
+
import { AcademyValidationField } from './academy-server.cjs';
|
|
3
|
+
export { AcademyValidationFieldConfig, AcademyValidationSource, DeriveValidationFieldsOptions, deriveValidationFields } from './academy-server.cjs';
|
|
83
4
|
|
|
84
5
|
/**
|
|
85
6
|
* `AcademyEnrollmentForm` (REB-584 follow-up) — the SDK-published academy
|
|
@@ -145,4 +66,4 @@ interface AcademyEnrollmentFormProps {
|
|
|
145
66
|
}
|
|
146
67
|
declare function AcademyEnrollmentForm(props: AcademyEnrollmentFormProps): react_jsx_runtime.JSX.Element;
|
|
147
68
|
|
|
148
|
-
export { AcademyEnrollmentForm, type AcademyEnrollmentFormCopy, type AcademyEnrollmentFormProps,
|
|
69
|
+
export { AcademyEnrollmentForm, type AcademyEnrollmentFormCopy, type AcademyEnrollmentFormProps, AcademyValidationField };
|