@alepha/react 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.browser.js +1488 -4
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1827 -4
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +58 -937
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +139 -2014
- package/dist/core/index.js.map +1 -1
- package/dist/form/index.d.ts.map +1 -1
- package/dist/form/index.js +6 -1
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.browser.js +3 -1
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +552 -8
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +17 -2
- package/dist/head/index.js.map +1 -1
- package/dist/{core → router}/index.browser.js +126 -516
- package/dist/router/index.browser.js.map +1 -0
- package/dist/router/index.d.ts +1334 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1939 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +12 -6
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/services/ReactAuth.ts +1 -1
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/components/ClientOnly.tsx +14 -0
- package/src/core/components/ErrorBoundary.tsx +3 -2
- package/src/core/contexts/AlephaContext.ts +3 -0
- package/src/core/contexts/AlephaProvider.tsx +2 -1
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/core/index.ts +13 -102
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/form/services/FormModel.ts +5 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +18 -8
- package/src/head/interfaces/Head.ts +3 -0
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/HeadProvider.ts +6 -1
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/head/providers/ServerHeadProvider.ts +20 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/{core → router}/components/ErrorViewer.tsx +2 -0
- package/src/router/components/Link.tsx +21 -0
- package/src/{core → router}/components/NestedView.tsx +3 -5
- package/src/router/components/NotFound.tsx +30 -0
- package/src/router/errors/Redirection.ts +28 -0
- package/src/{core → router}/hooks/useActive.ts +6 -2
- package/src/{core → router}/hooks/useQueryParams.ts +2 -2
- package/src/{core → router}/hooks/useRouter.ts +1 -1
- package/src/{core → router}/hooks/useRouterState.ts +1 -1
- package/src/{core → router}/index.browser.ts +14 -12
- package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
- package/src/router/index.ts +125 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/{core → router}/primitives/$page.ts +1 -1
- package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
- package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
- package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
- package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
- package/src/{core → router}/services/ReactPageServerService.ts +3 -0
- package/src/{core → router}/services/ReactPageService.ts +5 -5
- package/src/{core → router}/services/ReactRouter.ts +26 -5
- package/dist/core/index.browser.js.map +0 -1
- package/dist/core/index.native.js +0 -403
- package/dist/core/index.native.js.map +0 -1
- package/src/core/components/Link.tsx +0 -18
- package/src/core/components/NotFound.tsx +0 -27
- package/src/core/errors/Redirection.ts +0 -13
- package/src/core/hooks/useSchema.ts +0 -88
- package/src/core/index.native.ts +0 -21
- package/src/core/index.shared.ts +0 -9
- /package/src/{core → router}/contexts/RouterLayerContext.ts +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, test } from "vitest";
|
|
3
|
+
import { $dictionary } from "../primitives/$dictionary.ts";
|
|
4
|
+
import { AlephaReactI18n } from "../index.ts";
|
|
5
|
+
import { I18nProvider } from "./I18nProvider.ts";
|
|
6
|
+
|
|
7
|
+
describe("I18nProvider", () => {
|
|
8
|
+
test("should register dictionaries on initialization", async ({ expect }) => {
|
|
9
|
+
class App {
|
|
10
|
+
en = $dictionary({
|
|
11
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
fr = $dictionary({
|
|
15
|
+
lazy: async () => ({ default: { hello: "Bonjour" } }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
20
|
+
const app = alepha.inject(App);
|
|
21
|
+
const i18n = alepha.inject(I18nProvider);
|
|
22
|
+
|
|
23
|
+
expect(i18n.registry).toHaveLength(2);
|
|
24
|
+
expect(i18n.registry[0].lang).toBe("en");
|
|
25
|
+
expect(i18n.registry[1].lang).toBe("fr");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("should load all translations on server start", async ({ expect }) => {
|
|
29
|
+
class App {
|
|
30
|
+
en = $dictionary({
|
|
31
|
+
lazy: async () => ({ default: { hello: "Hello", world: "World" } }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
fr = $dictionary({
|
|
35
|
+
lazy: async () => ({ default: { hello: "Bonjour", world: "Monde" } }),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
40
|
+
const app = alepha.inject(App);
|
|
41
|
+
const i18n = alepha.inject(I18nProvider);
|
|
42
|
+
|
|
43
|
+
await alepha.start();
|
|
44
|
+
|
|
45
|
+
expect(i18n.registry[0].translations).toEqual({
|
|
46
|
+
hello: "Hello",
|
|
47
|
+
world: "World",
|
|
48
|
+
});
|
|
49
|
+
expect(i18n.registry[1].translations).toEqual({
|
|
50
|
+
hello: "Bonjour",
|
|
51
|
+
world: "Monde",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("should default to fallback language", async ({ expect }) => {
|
|
56
|
+
class App {
|
|
57
|
+
en = $dictionary({
|
|
58
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
63
|
+
const app = alepha.inject(App);
|
|
64
|
+
const i18n = alepha.inject(I18nProvider);
|
|
65
|
+
|
|
66
|
+
expect(i18n.lang).toBe("en");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("should translate keys in current language", async ({ expect }) => {
|
|
70
|
+
class App {
|
|
71
|
+
en = $dictionary({
|
|
72
|
+
lazy: async () => ({ default: { greeting: "Hello, World!" } }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
fr = $dictionary({
|
|
76
|
+
lazy: async () => ({ default: { greeting: "Bonjour, Monde!" } }),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
81
|
+
const app = alepha.inject(App);
|
|
82
|
+
const i18n = alepha.inject(I18nProvider);
|
|
83
|
+
|
|
84
|
+
await alepha.start();
|
|
85
|
+
|
|
86
|
+
expect(i18n.tr("greeting")).toBe("Hello, World!");
|
|
87
|
+
|
|
88
|
+
await i18n.setLang("fr");
|
|
89
|
+
expect(i18n.tr("greeting")).toBe("Bonjour, Monde!");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("should fallback to fallback language when key missing", async ({
|
|
93
|
+
expect,
|
|
94
|
+
}) => {
|
|
95
|
+
class App {
|
|
96
|
+
en = $dictionary({
|
|
97
|
+
lazy: async () => ({
|
|
98
|
+
default: { hello: "Hello", world: "World" },
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
fr = $dictionary({
|
|
103
|
+
lazy: async () => ({ default: { hello: "Bonjour" } }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
108
|
+
const app = alepha.inject(App);
|
|
109
|
+
const i18n = alepha.inject(I18nProvider);
|
|
110
|
+
|
|
111
|
+
await alepha.start();
|
|
112
|
+
await i18n.setLang("fr");
|
|
113
|
+
|
|
114
|
+
expect(i18n.tr("hello")).toBe("Bonjour");
|
|
115
|
+
expect(i18n.tr("world")).toBe("World"); // Falls back to English
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("should return key when translation not found", async ({ expect }) => {
|
|
119
|
+
class App {
|
|
120
|
+
en = $dictionary({
|
|
121
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
126
|
+
const app = alepha.inject(App);
|
|
127
|
+
const i18n = alepha.inject(I18nProvider);
|
|
128
|
+
|
|
129
|
+
await alepha.start();
|
|
130
|
+
|
|
131
|
+
expect(i18n.tr("missing.key")).toBe("missing.key");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("should support default option when key missing", async ({ expect }) => {
|
|
135
|
+
class App {
|
|
136
|
+
en = $dictionary({
|
|
137
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
142
|
+
const app = alepha.inject(App);
|
|
143
|
+
const i18n = alepha.inject(I18nProvider);
|
|
144
|
+
|
|
145
|
+
await alepha.start();
|
|
146
|
+
|
|
147
|
+
expect(i18n.tr("missing", { default: "Default Text" })).toBe(
|
|
148
|
+
"Default Text",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("should support variable interpolation", async ({ expect }) => {
|
|
153
|
+
class App {
|
|
154
|
+
en = $dictionary({
|
|
155
|
+
lazy: async () => ({
|
|
156
|
+
default: { greeting: "Hello, $1! You have $2 messages." },
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
162
|
+
const app = alepha.inject(App);
|
|
163
|
+
const i18n = alepha.inject(I18nProvider);
|
|
164
|
+
|
|
165
|
+
await alepha.start();
|
|
166
|
+
|
|
167
|
+
expect(i18n.tr("greeting", { args: ["John", "5"] })).toBe(
|
|
168
|
+
"Hello, John! You have 5 messages.",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("should list all available languages", async ({ expect }) => {
|
|
173
|
+
class App {
|
|
174
|
+
en = $dictionary({
|
|
175
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
fr = $dictionary({
|
|
179
|
+
lazy: async () => ({ default: { hello: "Bonjour" } }),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
de = $dictionary({
|
|
183
|
+
lazy: async () => ({ default: { hello: "Hallo" } }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
188
|
+
const app = alepha.inject(App);
|
|
189
|
+
const i18n = alepha.inject(I18nProvider);
|
|
190
|
+
|
|
191
|
+
const languages = i18n.languages;
|
|
192
|
+
expect(languages).toContain("en");
|
|
193
|
+
expect(languages).toContain("fr");
|
|
194
|
+
expect(languages).toContain("de");
|
|
195
|
+
expect(languages).toHaveLength(3);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("should support custom language codes", async ({ expect }) => {
|
|
199
|
+
class App {
|
|
200
|
+
enUS = $dictionary({
|
|
201
|
+
lang: "en-US",
|
|
202
|
+
lazy: async () => ({ default: { color: "color" } }),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
enGB = $dictionary({
|
|
206
|
+
lang: "en-GB",
|
|
207
|
+
lazy: async () => ({ default: { color: "colour" } }),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
212
|
+
const app = alepha.inject(App);
|
|
213
|
+
const i18n = alepha.inject(I18nProvider);
|
|
214
|
+
|
|
215
|
+
await alepha.start();
|
|
216
|
+
|
|
217
|
+
expect(i18n.registry[0].lang).toBe("en-US");
|
|
218
|
+
expect(i18n.registry[1].lang).toBe("en-GB");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("should support custom dictionary names", async ({ expect }) => {
|
|
222
|
+
class App {
|
|
223
|
+
english = $dictionary({
|
|
224
|
+
name: "custom-en",
|
|
225
|
+
lang: "en",
|
|
226
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
231
|
+
const app = alepha.inject(App);
|
|
232
|
+
const i18n = alepha.inject(I18nProvider);
|
|
233
|
+
|
|
234
|
+
expect(i18n.registry[0].name).toBe("custom-en");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("should update state when language changes", async ({ expect }) => {
|
|
238
|
+
class App {
|
|
239
|
+
en = $dictionary({
|
|
240
|
+
lazy: async () => ({ default: { hello: "Hello" } }),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
fr = $dictionary({
|
|
244
|
+
lazy: async () => ({ default: { hello: "Bonjour" } }),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
249
|
+
const app = alepha.inject(App);
|
|
250
|
+
const i18n = alepha.inject(I18nProvider);
|
|
251
|
+
|
|
252
|
+
await alepha.start();
|
|
253
|
+
|
|
254
|
+
// State is set lazily, so we should set a language first
|
|
255
|
+
await i18n.setLang("en");
|
|
256
|
+
expect(alepha.store.get("alepha.react.i18n.lang")).toBe("en");
|
|
257
|
+
|
|
258
|
+
await i18n.setLang("fr");
|
|
259
|
+
expect(alepha.store.get("alepha.react.i18n.lang")).toBe("fr");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should have number formatter for current language", async ({
|
|
263
|
+
expect,
|
|
264
|
+
}) => {
|
|
265
|
+
class App {
|
|
266
|
+
en = $dictionary({
|
|
267
|
+
lazy: async () => ({ default: {} }),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
272
|
+
const app = alepha.inject(App);
|
|
273
|
+
const i18n = alepha.inject(I18nProvider);
|
|
274
|
+
|
|
275
|
+
await alepha.start();
|
|
276
|
+
|
|
277
|
+
expect(i18n.numberFormat).toBeDefined();
|
|
278
|
+
expect(i18n.numberFormat.format).toBeTypeOf("function");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("should handle multiple translations with same key across dictionaries", async ({
|
|
282
|
+
expect,
|
|
283
|
+
}) => {
|
|
284
|
+
class App {
|
|
285
|
+
en = $dictionary({
|
|
286
|
+
lazy: async () => ({
|
|
287
|
+
default: {
|
|
288
|
+
app: "Application",
|
|
289
|
+
settings: "Settings",
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
fr = $dictionary({
|
|
295
|
+
lazy: async () => ({
|
|
296
|
+
default: {
|
|
297
|
+
app: "Programme",
|
|
298
|
+
settings: "Paramètres",
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
es = $dictionary({
|
|
304
|
+
lazy: async () => ({
|
|
305
|
+
default: {
|
|
306
|
+
app: "Aplicación",
|
|
307
|
+
settings: "Configuración",
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
314
|
+
const app = alepha.inject(App);
|
|
315
|
+
const i18n = alepha.inject(I18nProvider);
|
|
316
|
+
|
|
317
|
+
await alepha.start();
|
|
318
|
+
|
|
319
|
+
expect(i18n.tr("app")).toBe("Application");
|
|
320
|
+
expect(i18n.tr("settings")).toBe("Settings");
|
|
321
|
+
|
|
322
|
+
await i18n.setLang("fr");
|
|
323
|
+
expect(i18n.tr("app")).toBe("Programme");
|
|
324
|
+
expect(i18n.tr("settings")).toBe("Paramètres");
|
|
325
|
+
|
|
326
|
+
await i18n.setLang("es");
|
|
327
|
+
expect(i18n.tr("app")).toBe("Aplicación");
|
|
328
|
+
expect(i18n.tr("settings")).toBe("Configuración");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("should handle empty dictionaries", async ({ expect }) => {
|
|
332
|
+
class App {
|
|
333
|
+
en = $dictionary({
|
|
334
|
+
lazy: async () => ({ default: {} }),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
339
|
+
const app = alepha.inject(App);
|
|
340
|
+
const i18n = alepha.inject(I18nProvider);
|
|
341
|
+
|
|
342
|
+
await alepha.start();
|
|
343
|
+
|
|
344
|
+
expect(i18n.tr("anything")).toBe("anything");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("should handle complex interpolation patterns", async ({ expect }) => {
|
|
348
|
+
class App {
|
|
349
|
+
en = $dictionary({
|
|
350
|
+
lazy: async () => ({
|
|
351
|
+
default: {
|
|
352
|
+
complex: "User $1 has $2 followers and follows $3 people",
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
359
|
+
const app = alepha.inject(App);
|
|
360
|
+
const i18n = alepha.inject(I18nProvider);
|
|
361
|
+
|
|
362
|
+
await alepha.start();
|
|
363
|
+
|
|
364
|
+
expect(i18n.tr("complex", { args: ["Alice", "1000", "500"] })).toBe(
|
|
365
|
+
"User Alice has 1000 followers and follows 500 people",
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("should handle partial interpolation args", async ({ expect }) => {
|
|
370
|
+
class App {
|
|
371
|
+
en = $dictionary({
|
|
372
|
+
lazy: async () => ({
|
|
373
|
+
default: { message: "Hello $1, you have $2 messages and $3 alerts" },
|
|
374
|
+
}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
379
|
+
const app = alepha.inject(App);
|
|
380
|
+
const i18n = alepha.inject(I18nProvider);
|
|
381
|
+
|
|
382
|
+
await alepha.start();
|
|
383
|
+
|
|
384
|
+
// Provide fewer args than placeholders
|
|
385
|
+
expect(i18n.tr("message", { args: ["Bob"] })).toBe(
|
|
386
|
+
"Hello Bob, you have $2 messages and $3 alerts",
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -47,6 +47,8 @@ const ErrorViewer = ({ error, alepha }: ErrorViewerProps) => {
|
|
|
47
47
|
|
|
48
48
|
export default ErrorViewer;
|
|
49
49
|
|
|
50
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
51
|
+
|
|
50
52
|
/**
|
|
51
53
|
* Parse stack trace string into structured frames
|
|
52
54
|
*/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type AnchorHTMLAttributes, createElement } from "react";
|
|
2
|
+
import { useRouter } from "../hooks/useRouter.ts";
|
|
3
|
+
|
|
4
|
+
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Link component for client-side navigation.
|
|
10
|
+
*
|
|
11
|
+
* It's a simple wrapper around an anchor (`<a>`) element using the `useRouter` hook.
|
|
12
|
+
*/
|
|
13
|
+
const Link = (props: LinkProps) => {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
|
|
16
|
+
return createElement(
|
|
17
|
+
"a", { ...props, ...router.anchor(props.href) }, props.children
|
|
18
|
+
)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default Link;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { memo, type ReactNode, use, useRef, useState } from "react";
|
|
2
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
2
3
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
3
|
-
import type { ErrorHandler, PageAnimation } from "../primitives/$page.ts";
|
|
4
4
|
import { Redirection } from "../errors/Redirection.ts";
|
|
5
|
-
import { useEvents } from "../hooks/useEvents.ts";
|
|
6
5
|
import { useRouterState } from "../hooks/useRouterState.ts";
|
|
7
|
-
import type {
|
|
8
|
-
import ErrorBoundary from "./ErrorBoundary.tsx";
|
|
6
|
+
import type { PageAnimation } from "../primitives/$page.ts";
|
|
9
7
|
import ErrorViewer from "./ErrorViewer.tsx";
|
|
10
|
-
import { useAlepha } from "
|
|
8
|
+
import { ErrorBoundary, useAlepha, useEvents } from "@alepha/react";
|
|
11
9
|
|
|
12
10
|
export interface NestedViewProps {
|
|
13
11
|
children?: ReactNode;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default 404 Not Found page component.
|
|
5
|
+
*/
|
|
6
|
+
const NotFound = (props: { style?: CSSProperties }) => (
|
|
7
|
+
<div
|
|
8
|
+
style={{
|
|
9
|
+
width: "100%",
|
|
10
|
+
minHeight: "90vh",
|
|
11
|
+
boxSizing: "border-box",
|
|
12
|
+
display: "flex",
|
|
13
|
+
flexDirection: "column",
|
|
14
|
+
justifyContent: "center",
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
textAlign: "center",
|
|
17
|
+
fontFamily:
|
|
18
|
+
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
19
|
+
padding: "2rem",
|
|
20
|
+
...props.style,
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<div style={{ fontSize: "6rem", fontWeight: 200, lineHeight: 1 }}>404</div>
|
|
24
|
+
<div style={{ fontSize: "0.875rem", marginTop: "1rem", opacity: 0.6 }}>
|
|
25
|
+
Page not found
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export default NotFound;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AlephaError } from "alepha";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Used for Redirection during the page loading.
|
|
5
|
+
*
|
|
6
|
+
* Depends on the context, it can be thrown or just returned.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Redirection } from "@alepha/react";
|
|
11
|
+
*
|
|
12
|
+
* const MyPage = $page({
|
|
13
|
+
* resolve: async () => {
|
|
14
|
+
* if (needRedirect) {
|
|
15
|
+
* throw new Redirection("/new-path");
|
|
16
|
+
* }
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class Redirection extends AlephaError {
|
|
22
|
+
public readonly redirect: string;
|
|
23
|
+
|
|
24
|
+
constructor(redirect: string) {
|
|
25
|
+
super("Redirection");
|
|
26
|
+
this.redirect = redirect;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -8,11 +8,15 @@ export interface UseActiveOptions {
|
|
|
8
8
|
startWith?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Hook to determine if a given route is active and to provide anchor props for navigation.
|
|
13
|
+
* This hook refreshes on router state changes.
|
|
14
|
+
*/
|
|
11
15
|
export const useActive = (args: string | UseActiveOptions): UseActiveHook => {
|
|
16
|
+
useRouterState();
|
|
17
|
+
|
|
12
18
|
const router = useRouter();
|
|
13
19
|
const [isPending, setPending] = useState(false);
|
|
14
|
-
const state = useRouterState();
|
|
15
|
-
const current = state.url.pathname;
|
|
16
20
|
|
|
17
21
|
const options: UseActiveOptions =
|
|
18
22
|
typeof args === "string" ? { href: args } : { ...args, href: args.href };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Alepha, Static, TObject } from "alepha";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { useAlepha } from "
|
|
3
|
+
import { useAlepha } from "@alepha/react";
|
|
4
4
|
import { useRouter } from "./useRouter.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Hook to manage query parameters in the URL using a defined schema.
|
|
8
8
|
*/
|
|
9
9
|
export const useQueryParams = <T extends TObject>(
|
|
10
10
|
schema: T,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AlephaError } from "alepha";
|
|
2
2
|
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
3
|
-
import { useStore } from "
|
|
3
|
+
import { useStore } from "@alepha/react";
|
|
4
4
|
|
|
5
5
|
export const useRouterState = (): ReactRouterState => {
|
|
6
6
|
const [state] = useStore("alepha.react.router.state");
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { $module } from "alepha";
|
|
2
|
-
import { AlephaDateTime } from "alepha/datetime";
|
|
3
|
-
import { AlephaServer } from "alepha/server";
|
|
4
|
-
import { AlephaServerLinks } from "alepha/server/links";
|
|
5
2
|
import { $page } from "./primitives/$page.ts";
|
|
6
|
-
import {
|
|
3
|
+
import { ReactRouter } from "./services/ReactRouter.ts";
|
|
7
4
|
import { ReactBrowserRendererProvider } from "./providers/ReactBrowserRendererProvider.ts";
|
|
8
5
|
import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
|
|
9
|
-
import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
|
|
10
6
|
import { ReactPageService } from "./services/ReactPageService.ts";
|
|
11
|
-
import {
|
|
7
|
+
import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
|
|
8
|
+
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
|
|
9
|
+
import { AlephaDateTime } from "alepha/datetime";
|
|
10
|
+
import { AlephaServer } from "alepha/server";
|
|
11
|
+
import { AlephaServerLinks } from "alepha/server/links";
|
|
12
|
+
import { AlephaReact } from "@alepha/react";
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
14
15
|
|
|
15
16
|
export * from "./index.shared.ts";
|
|
16
|
-
export * from "./
|
|
17
|
-
export * from "./providers/
|
|
18
|
-
export * from "./providers/
|
|
19
|
-
export * from "./providers/ReactPageProvider.ts";
|
|
17
|
+
export * from "./providers/ReactBrowserProvider.ts"
|
|
18
|
+
export * from "./providers/ReactBrowserRouterProvider.ts"
|
|
19
|
+
export * from "./providers/ReactBrowserRendererProvider.ts"
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
22
22
|
|
|
23
|
-
export const
|
|
24
|
-
name: "alepha.react",
|
|
23
|
+
export const AlephaReactRouter = $module({
|
|
24
|
+
name: "alepha.react.router",
|
|
25
25
|
primitives: [$page],
|
|
26
26
|
services: [
|
|
27
27
|
ReactPageProvider,
|
|
@@ -33,6 +33,7 @@ export const AlephaReact = $module({
|
|
|
33
33
|
],
|
|
34
34
|
register: (alepha) =>
|
|
35
35
|
alepha
|
|
36
|
+
.with(AlephaReact)
|
|
36
37
|
.with(AlephaDateTime)
|
|
37
38
|
.with(AlephaServer)
|
|
38
39
|
.with(AlephaServerLinks)
|
|
@@ -42,3 +43,4 @@ export const AlephaReact = $module({
|
|
|
42
43
|
.with(ReactBrowserRendererProvider)
|
|
43
44
|
.with(ReactRouter),
|
|
44
45
|
});
|
|
46
|
+
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export { default as ClientOnly } from "./components/ClientOnly.tsx";
|
|
2
|
-
export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
|
3
1
|
export { default as ErrorViewer } from "./components/ErrorViewer.tsx";
|
|
2
|
+
export type * from "./components/ErrorViewer.tsx";
|
|
4
3
|
export { default as Link, type LinkProps } from "./components/Link.tsx";
|
|
4
|
+
export type * from "./components/Link.tsx";
|
|
5
5
|
export { default as NestedView } from "./components/NestedView.tsx";
|
|
6
|
+
export type * from "./components/NestedView.tsx";
|
|
6
7
|
export { default as NotFound } from "./components/NotFound.tsx";
|
|
8
|
+
export type * from "./components/NotFound.tsx";
|
|
7
9
|
export * from "./contexts/RouterLayerContext.ts";
|
|
8
10
|
export * from "./primitives/$page.ts";
|
|
9
11
|
export * from "./errors/Redirection.ts";
|
|
@@ -11,5 +13,6 @@ export * from "./hooks/useActive.ts";
|
|
|
11
13
|
export * from "./hooks/useQueryParams.ts";
|
|
12
14
|
export * from "./hooks/useRouter.ts";
|
|
13
15
|
export * from "./hooks/useRouterState.ts";
|
|
14
|
-
export * from "./hooks/useSchema.ts";
|
|
15
16
|
export * from "./services/ReactRouter.ts";
|
|
17
|
+
export * from "./services/ReactPageService.ts"
|
|
18
|
+
export * from "./providers/ReactPageProvider.ts";
|