@charleslemaux/cms 1.0.0-rc.1

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.
@@ -0,0 +1,268 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ interface ArticleBodyProps extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'dangerouslySetInnerHTML'> {
5
+ /** The article's `bodyHtml`. */
6
+ html: string;
7
+ /**
8
+ * Cleans the HTML once more before it is rendered, e.g. `(html) => DOMPurify.sanitize(html)`.
9
+ * Odoo already cleans `bodyHtml` with a strict allowlist: this is defence in depth.
10
+ */
11
+ sanitize?: (html: string) => string;
12
+ /** The wrapping element. Default: div. */
13
+ as?: 'div' | 'article' | 'section';
14
+ }
15
+ declare function ArticleBody({ html, sanitize, as: Tag, ...rest }: ArticleBodyProps): react.JSX.Element;
16
+
17
+ /** The payloads of the ICRC - CMS public API v1 (spec 7.3). JSON keys are camelCase. */
18
+ interface Language {
19
+ /** API language code, e.g. "fr". */
20
+ code: string;
21
+ /** The name a visitor reads, e.g. "Français". */
22
+ name: string;
23
+ }
24
+ interface CategoryCount {
25
+ slug: string;
26
+ name: string;
27
+ /** Live articles of the zone in this category. */
28
+ count: number;
29
+ }
30
+ interface CaptchaSettings {
31
+ provider: 'turnstile';
32
+ siteKey: string;
33
+ }
34
+ interface NewsletterSettings {
35
+ enabled: boolean;
36
+ /** Null when the zone has no captcha: signups are then protected by rate limits only. */
37
+ captcha: CaptchaSettings | null;
38
+ }
39
+ interface Zone {
40
+ code: string;
41
+ name: string;
42
+ siteUrl: string;
43
+ /** The default language first. */
44
+ languages: Language[];
45
+ defaultLanguage: string;
46
+ newsletter: NewsletterSettings;
47
+ categories: CategoryCount[];
48
+ }
49
+ interface ImageSource {
50
+ width: number;
51
+ src: string;
52
+ }
53
+ interface Cover {
54
+ /** The largest version (1920 pixels wide at most). */
55
+ src: string;
56
+ width: number;
57
+ height: number;
58
+ alt: string;
59
+ caption: string;
60
+ /** Every version, smallest first: ready for srcset. */
61
+ sources: ImageSource[];
62
+ }
63
+ interface Category {
64
+ slug: string;
65
+ name: string;
66
+ }
67
+ interface Tag {
68
+ slug: string;
69
+ name: string;
70
+ }
71
+ interface Author {
72
+ name: string;
73
+ role: string;
74
+ }
75
+ interface ArticleSummary {
76
+ slug: string;
77
+ /** The article on this zone's website, in the served language. */
78
+ url: string;
79
+ /** The article on its primary website, in the served language. */
80
+ canonicalUrl: string;
81
+ /** The language served: the source language when `fallback` is true. */
82
+ lang: string;
83
+ sourceLang: string;
84
+ /** True when the article has no version in the asked language. */
85
+ fallback: boolean;
86
+ availableLangs: string[];
87
+ title: string;
88
+ subtitle: string;
89
+ excerpt: string;
90
+ cover: Cover | null;
91
+ category: Category | null;
92
+ tags: Tag[];
93
+ author: Author;
94
+ /** When the article went live on this zone (ISO 8601, UTC). */
95
+ publishedAt: string;
96
+ /** The latest change of the article or of the served translation (ISO 8601, UTC). */
97
+ updatedAt: string;
98
+ readingTimeMinutes: number;
99
+ }
100
+ interface Alternate {
101
+ lang: string;
102
+ url: string;
103
+ }
104
+ interface Article extends ArticleSummary {
105
+ /** Cleaned by Odoo with a strict allowlist (spec 7.6). */
106
+ bodyHtml: string;
107
+ /** The real translations, when the zone's article URL pattern has {lang}; else empty. */
108
+ alternates: Alternate[];
109
+ /** Up to 3 articles of the same zone. */
110
+ related: ArticleSummary[];
111
+ }
112
+ interface Page<T> {
113
+ items: T[];
114
+ page: number;
115
+ limit: number;
116
+ total: number;
117
+ }
118
+ interface SubscribeResult {
119
+ status: 'pending';
120
+ }
121
+ interface ConfirmResult {
122
+ status: 'confirmed';
123
+ zone: string;
124
+ lang: string;
125
+ }
126
+ interface UnsubscribeResult {
127
+ status: 'unsubscribed';
128
+ zone: string;
129
+ }
130
+
131
+ interface ReadOptions {
132
+ /** "fr", "fr-FR" or "fr_FR". Default: the zone's default language. */
133
+ lang?: string;
134
+ /** Cancels the request. */
135
+ signal?: AbortSignal;
136
+ }
137
+ interface ListArticlesParams extends ReadOptions {
138
+ /** A category slug. */
139
+ category?: string;
140
+ /** A tag slug. */
141
+ tag?: string;
142
+ /** From 1. */
143
+ page?: number;
144
+ /** From 1 to 50. Default: 12. */
145
+ limit?: number;
146
+ }
147
+ interface SubscribeParams {
148
+ email: string;
149
+ /** Language of the newsletter the visitor joins. Default: the zone's default language. */
150
+ lang?: string;
151
+ /** The visitor ticked the consent box. */
152
+ consent: true;
153
+ /** The Cloudflare Turnstile token, when the zone has a captcha. */
154
+ captchaToken?: string;
155
+ /** The honeypot: people leave it empty. */
156
+ website?: string;
157
+ }
158
+ interface CmsClient {
159
+ readonly baseUrl: string;
160
+ readonly zone: string;
161
+ getZone(options?: ReadOptions): Promise<Zone>;
162
+ listArticles(params?: ListArticlesParams): Promise<Page<ArticleSummary>>;
163
+ /** Null when the article is not live on this zone. */
164
+ getArticle(slug: string, options?: ReadOptions): Promise<Article | null>;
165
+ /** Call it from the visitor's browser: Odoo checks the page's origin and limits signups per visitor. */
166
+ subscribe(params: SubscribeParams): Promise<SubscribeResult>;
167
+ confirm(token: string): Promise<ConfirmResult>;
168
+ unsubscribe(token: string): Promise<UnsubscribeResult>;
169
+ }
170
+
171
+ /** What went wrong: the API's codes (spec 12), and three the client adds. */
172
+ type CmsErrorCode = 'not_found' | 'invalid_request' | 'invalid_email' | 'consent_required' | 'origin_not_allowed' | 'captcha_failed' | 'captcha_unavailable' | 'invalid_token' | 'payload_too_large' | 'unsupported_media_type' | 'rate_limited' | 'network_error' | 'invalid_response' | 'http_error';
173
+ declare class CmsError extends Error {
174
+ /** The HTTP status; 0 when the CMS could not be reached. */
175
+ readonly status: number;
176
+ readonly code: CmsErrorCode | (string & {});
177
+ /** Seconds to wait before trying again, on a 429. */
178
+ readonly retryAfter?: number;
179
+ constructor(status: number, code: CmsErrorCode | (string & {}), message: string, retryAfter?: number);
180
+ }
181
+
182
+ interface AsyncState<T> {
183
+ /** The answer, once it arrived. */
184
+ data: T | undefined;
185
+ error: CmsError | undefined;
186
+ loading: boolean;
187
+ }
188
+ declare function useZone(client: CmsClient, options?: {
189
+ lang?: string;
190
+ }): AsyncState<Zone>;
191
+ declare function useArticles(client: CmsClient, params?: Omit<ListArticlesParams, 'signal'>): AsyncState<Page<ArticleSummary>>;
192
+ /** `data` is null when the article is not live on the zone. */
193
+ declare function useArticle(client: CmsClient, slug: string | null | undefined, options?: {
194
+ lang?: string;
195
+ }): AsyncState<Article | null>;
196
+
197
+ interface NewsletterFormLabels {
198
+ email: string;
199
+ emailPlaceholder: string;
200
+ /** The consent checkbox's label; may hold a link to the privacy policy. */
201
+ consent: ReactNode;
202
+ submit: string;
203
+ sending: string;
204
+ sent: string;
205
+ invalidEmail: string;
206
+ consentRequired: string;
207
+ captchaRequired: string;
208
+ captchaFailed: string;
209
+ rateLimited: string;
210
+ unavailable: string;
211
+ }
212
+ interface NewsletterPageLabels {
213
+ confirmIntro: string;
214
+ confirmButton: string;
215
+ confirming: string;
216
+ confirmed: string;
217
+ unsubscribeIntro: string;
218
+ unsubscribeButton: string;
219
+ unsubscribing: string;
220
+ unsubscribed: string;
221
+ invalidLink: string;
222
+ noLink: string;
223
+ failed: string;
224
+ }
225
+ /** The form's labels in `lang` (English when there is no such set), with `overrides` on top. */
226
+ declare function newsletterFormLabels(lang?: string, overrides?: Partial<NewsletterFormLabels>): NewsletterFormLabels;
227
+ /** The newsletter page's labels in `lang` (English when there is no such set), with `overrides` on top. */
228
+ declare function newsletterPageLabels(lang?: string, overrides?: Partial<NewsletterPageLabels>): NewsletterPageLabels;
229
+
230
+ interface NewsletterFormProps {
231
+ client: CmsClient;
232
+ /** The visitor's language, e.g. "fr": the newsletter they join, and the captcha's and labels' language. */
233
+ lang?: string;
234
+ /** The zone's newsletter settings (`zone.newsletter`). Omitted: read with `client.getZone()`. */
235
+ settings?: NewsletterSettings;
236
+ /** Replaces some of the default labels. */
237
+ labels?: Partial<NewsletterFormLabels>;
238
+ className?: string;
239
+ captchaTheme?: 'auto' | 'light' | 'dark';
240
+ /** The confirmation email is on its way to this address. */
241
+ onSuccess?: (email: string) => void;
242
+ onError?: (error: CmsError) => void;
243
+ }
244
+ declare function NewsletterForm({ client, lang, settings: givenSettings, labels: overrides, className, captchaTheme, onSuccess, onError, }: NewsletterFormProps): react.JSX.Element | null;
245
+
246
+ /** Where a newsletter page reads its link: a URL, a path, a query string, URLSearchParams or Next.js searchParams. */
247
+ type NewsletterSource = string | URL | URLSearchParams | Record<string, string | string[] | undefined>;
248
+
249
+ interface NewsletterPageProps {
250
+ client: CmsClient;
251
+ /** Where the link's `?confirm=` or `?unsubscribe=` is read, e.g. Next.js `searchParams`. Default: the address bar. */
252
+ search?: NewsletterSource;
253
+ /** The labels' language. */
254
+ lang?: string;
255
+ labels?: Partial<NewsletterPageLabels>;
256
+ className?: string;
257
+ /** The subscription is confirmed, or cancelled. */
258
+ onDone?: (result: ConfirmResult | UnsubscribeResult) => void;
259
+ /** Remove the token from the address bar once done. Default: true. */
260
+ cleanUrl?: boolean;
261
+ }
262
+ /**
263
+ * The page a newsletter link opens. It shows one button and calls the API only on its click, so
264
+ * mailbox link scanners never confirm or unsubscribe anybody (spec 10).
265
+ */
266
+ declare function NewsletterPage({ client, search, lang, labels: overrides, className, onDone, cleanUrl, }: NewsletterPageProps): react.JSX.Element;
267
+
268
+ export { ArticleBody, type ArticleBodyProps, type AsyncState, NewsletterForm, type NewsletterFormLabels, type NewsletterFormProps, NewsletterPage, type NewsletterPageLabels, type NewsletterPageProps, newsletterFormLabels, newsletterPageLabels, useArticle, useArticles, useZone };
@@ -0,0 +1,268 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ interface ArticleBodyProps extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'dangerouslySetInnerHTML'> {
5
+ /** The article's `bodyHtml`. */
6
+ html: string;
7
+ /**
8
+ * Cleans the HTML once more before it is rendered, e.g. `(html) => DOMPurify.sanitize(html)`.
9
+ * Odoo already cleans `bodyHtml` with a strict allowlist: this is defence in depth.
10
+ */
11
+ sanitize?: (html: string) => string;
12
+ /** The wrapping element. Default: div. */
13
+ as?: 'div' | 'article' | 'section';
14
+ }
15
+ declare function ArticleBody({ html, sanitize, as: Tag, ...rest }: ArticleBodyProps): react.JSX.Element;
16
+
17
+ /** The payloads of the ICRC - CMS public API v1 (spec 7.3). JSON keys are camelCase. */
18
+ interface Language {
19
+ /** API language code, e.g. "fr". */
20
+ code: string;
21
+ /** The name a visitor reads, e.g. "Français". */
22
+ name: string;
23
+ }
24
+ interface CategoryCount {
25
+ slug: string;
26
+ name: string;
27
+ /** Live articles of the zone in this category. */
28
+ count: number;
29
+ }
30
+ interface CaptchaSettings {
31
+ provider: 'turnstile';
32
+ siteKey: string;
33
+ }
34
+ interface NewsletterSettings {
35
+ enabled: boolean;
36
+ /** Null when the zone has no captcha: signups are then protected by rate limits only. */
37
+ captcha: CaptchaSettings | null;
38
+ }
39
+ interface Zone {
40
+ code: string;
41
+ name: string;
42
+ siteUrl: string;
43
+ /** The default language first. */
44
+ languages: Language[];
45
+ defaultLanguage: string;
46
+ newsletter: NewsletterSettings;
47
+ categories: CategoryCount[];
48
+ }
49
+ interface ImageSource {
50
+ width: number;
51
+ src: string;
52
+ }
53
+ interface Cover {
54
+ /** The largest version (1920 pixels wide at most). */
55
+ src: string;
56
+ width: number;
57
+ height: number;
58
+ alt: string;
59
+ caption: string;
60
+ /** Every version, smallest first: ready for srcset. */
61
+ sources: ImageSource[];
62
+ }
63
+ interface Category {
64
+ slug: string;
65
+ name: string;
66
+ }
67
+ interface Tag {
68
+ slug: string;
69
+ name: string;
70
+ }
71
+ interface Author {
72
+ name: string;
73
+ role: string;
74
+ }
75
+ interface ArticleSummary {
76
+ slug: string;
77
+ /** The article on this zone's website, in the served language. */
78
+ url: string;
79
+ /** The article on its primary website, in the served language. */
80
+ canonicalUrl: string;
81
+ /** The language served: the source language when `fallback` is true. */
82
+ lang: string;
83
+ sourceLang: string;
84
+ /** True when the article has no version in the asked language. */
85
+ fallback: boolean;
86
+ availableLangs: string[];
87
+ title: string;
88
+ subtitle: string;
89
+ excerpt: string;
90
+ cover: Cover | null;
91
+ category: Category | null;
92
+ tags: Tag[];
93
+ author: Author;
94
+ /** When the article went live on this zone (ISO 8601, UTC). */
95
+ publishedAt: string;
96
+ /** The latest change of the article or of the served translation (ISO 8601, UTC). */
97
+ updatedAt: string;
98
+ readingTimeMinutes: number;
99
+ }
100
+ interface Alternate {
101
+ lang: string;
102
+ url: string;
103
+ }
104
+ interface Article extends ArticleSummary {
105
+ /** Cleaned by Odoo with a strict allowlist (spec 7.6). */
106
+ bodyHtml: string;
107
+ /** The real translations, when the zone's article URL pattern has {lang}; else empty. */
108
+ alternates: Alternate[];
109
+ /** Up to 3 articles of the same zone. */
110
+ related: ArticleSummary[];
111
+ }
112
+ interface Page<T> {
113
+ items: T[];
114
+ page: number;
115
+ limit: number;
116
+ total: number;
117
+ }
118
+ interface SubscribeResult {
119
+ status: 'pending';
120
+ }
121
+ interface ConfirmResult {
122
+ status: 'confirmed';
123
+ zone: string;
124
+ lang: string;
125
+ }
126
+ interface UnsubscribeResult {
127
+ status: 'unsubscribed';
128
+ zone: string;
129
+ }
130
+
131
+ interface ReadOptions {
132
+ /** "fr", "fr-FR" or "fr_FR". Default: the zone's default language. */
133
+ lang?: string;
134
+ /** Cancels the request. */
135
+ signal?: AbortSignal;
136
+ }
137
+ interface ListArticlesParams extends ReadOptions {
138
+ /** A category slug. */
139
+ category?: string;
140
+ /** A tag slug. */
141
+ tag?: string;
142
+ /** From 1. */
143
+ page?: number;
144
+ /** From 1 to 50. Default: 12. */
145
+ limit?: number;
146
+ }
147
+ interface SubscribeParams {
148
+ email: string;
149
+ /** Language of the newsletter the visitor joins. Default: the zone's default language. */
150
+ lang?: string;
151
+ /** The visitor ticked the consent box. */
152
+ consent: true;
153
+ /** The Cloudflare Turnstile token, when the zone has a captcha. */
154
+ captchaToken?: string;
155
+ /** The honeypot: people leave it empty. */
156
+ website?: string;
157
+ }
158
+ interface CmsClient {
159
+ readonly baseUrl: string;
160
+ readonly zone: string;
161
+ getZone(options?: ReadOptions): Promise<Zone>;
162
+ listArticles(params?: ListArticlesParams): Promise<Page<ArticleSummary>>;
163
+ /** Null when the article is not live on this zone. */
164
+ getArticle(slug: string, options?: ReadOptions): Promise<Article | null>;
165
+ /** Call it from the visitor's browser: Odoo checks the page's origin and limits signups per visitor. */
166
+ subscribe(params: SubscribeParams): Promise<SubscribeResult>;
167
+ confirm(token: string): Promise<ConfirmResult>;
168
+ unsubscribe(token: string): Promise<UnsubscribeResult>;
169
+ }
170
+
171
+ /** What went wrong: the API's codes (spec 12), and three the client adds. */
172
+ type CmsErrorCode = 'not_found' | 'invalid_request' | 'invalid_email' | 'consent_required' | 'origin_not_allowed' | 'captcha_failed' | 'captcha_unavailable' | 'invalid_token' | 'payload_too_large' | 'unsupported_media_type' | 'rate_limited' | 'network_error' | 'invalid_response' | 'http_error';
173
+ declare class CmsError extends Error {
174
+ /** The HTTP status; 0 when the CMS could not be reached. */
175
+ readonly status: number;
176
+ readonly code: CmsErrorCode | (string & {});
177
+ /** Seconds to wait before trying again, on a 429. */
178
+ readonly retryAfter?: number;
179
+ constructor(status: number, code: CmsErrorCode | (string & {}), message: string, retryAfter?: number);
180
+ }
181
+
182
+ interface AsyncState<T> {
183
+ /** The answer, once it arrived. */
184
+ data: T | undefined;
185
+ error: CmsError | undefined;
186
+ loading: boolean;
187
+ }
188
+ declare function useZone(client: CmsClient, options?: {
189
+ lang?: string;
190
+ }): AsyncState<Zone>;
191
+ declare function useArticles(client: CmsClient, params?: Omit<ListArticlesParams, 'signal'>): AsyncState<Page<ArticleSummary>>;
192
+ /** `data` is null when the article is not live on the zone. */
193
+ declare function useArticle(client: CmsClient, slug: string | null | undefined, options?: {
194
+ lang?: string;
195
+ }): AsyncState<Article | null>;
196
+
197
+ interface NewsletterFormLabels {
198
+ email: string;
199
+ emailPlaceholder: string;
200
+ /** The consent checkbox's label; may hold a link to the privacy policy. */
201
+ consent: ReactNode;
202
+ submit: string;
203
+ sending: string;
204
+ sent: string;
205
+ invalidEmail: string;
206
+ consentRequired: string;
207
+ captchaRequired: string;
208
+ captchaFailed: string;
209
+ rateLimited: string;
210
+ unavailable: string;
211
+ }
212
+ interface NewsletterPageLabels {
213
+ confirmIntro: string;
214
+ confirmButton: string;
215
+ confirming: string;
216
+ confirmed: string;
217
+ unsubscribeIntro: string;
218
+ unsubscribeButton: string;
219
+ unsubscribing: string;
220
+ unsubscribed: string;
221
+ invalidLink: string;
222
+ noLink: string;
223
+ failed: string;
224
+ }
225
+ /** The form's labels in `lang` (English when there is no such set), with `overrides` on top. */
226
+ declare function newsletterFormLabels(lang?: string, overrides?: Partial<NewsletterFormLabels>): NewsletterFormLabels;
227
+ /** The newsletter page's labels in `lang` (English when there is no such set), with `overrides` on top. */
228
+ declare function newsletterPageLabels(lang?: string, overrides?: Partial<NewsletterPageLabels>): NewsletterPageLabels;
229
+
230
+ interface NewsletterFormProps {
231
+ client: CmsClient;
232
+ /** The visitor's language, e.g. "fr": the newsletter they join, and the captcha's and labels' language. */
233
+ lang?: string;
234
+ /** The zone's newsletter settings (`zone.newsletter`). Omitted: read with `client.getZone()`. */
235
+ settings?: NewsletterSettings;
236
+ /** Replaces some of the default labels. */
237
+ labels?: Partial<NewsletterFormLabels>;
238
+ className?: string;
239
+ captchaTheme?: 'auto' | 'light' | 'dark';
240
+ /** The confirmation email is on its way to this address. */
241
+ onSuccess?: (email: string) => void;
242
+ onError?: (error: CmsError) => void;
243
+ }
244
+ declare function NewsletterForm({ client, lang, settings: givenSettings, labels: overrides, className, captchaTheme, onSuccess, onError, }: NewsletterFormProps): react.JSX.Element | null;
245
+
246
+ /** Where a newsletter page reads its link: a URL, a path, a query string, URLSearchParams or Next.js searchParams. */
247
+ type NewsletterSource = string | URL | URLSearchParams | Record<string, string | string[] | undefined>;
248
+
249
+ interface NewsletterPageProps {
250
+ client: CmsClient;
251
+ /** Where the link's `?confirm=` or `?unsubscribe=` is read, e.g. Next.js `searchParams`. Default: the address bar. */
252
+ search?: NewsletterSource;
253
+ /** The labels' language. */
254
+ lang?: string;
255
+ labels?: Partial<NewsletterPageLabels>;
256
+ className?: string;
257
+ /** The subscription is confirmed, or cancelled. */
258
+ onDone?: (result: ConfirmResult | UnsubscribeResult) => void;
259
+ /** Remove the token from the address bar once done. Default: true. */
260
+ cleanUrl?: boolean;
261
+ }
262
+ /**
263
+ * The page a newsletter link opens. It shows one button and calls the API only on its click, so
264
+ * mailbox link scanners never confirm or unsubscribe anybody (spec 10).
265
+ */
266
+ declare function NewsletterPage({ client, search, lang, labels: overrides, className, onDone, cleanUrl, }: NewsletterPageProps): react.JSX.Element;
267
+
268
+ export { ArticleBody, type ArticleBodyProps, type AsyncState, NewsletterForm, type NewsletterFormLabels, type NewsletterFormProps, NewsletterPage, type NewsletterPageLabels, type NewsletterPageProps, newsletterFormLabels, newsletterPageLabels, useArticle, useArticles, useZone };