@aminnairi/react-router 2.0.1 → 3.0.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/README.md +235 -157
- package/index.tsx +417 -271
- package/package.json +6 -2
- package/.eslintrc.cjs +0 -13
- package/tsconfig.json +0 -16
package/index.tsx
CHANGED
|
@@ -1,381 +1,527 @@
|
|
|
1
|
-
|
|
2
|
-
import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren, createContext, SetStateAction, Dispatch, ReactNode, useContext, useCallback, memo, MouseEvent } from "react";
|
|
3
|
-
|
|
4
|
-
export type AbsolutePath<Path extends string> =
|
|
5
|
-
Path extends `${infer Start}:${string}/${infer Rest}`
|
|
6
|
-
? `${Start}${string}/${AbsolutePath<Rest>}`
|
|
7
|
-
: Path extends `${infer Start}:${string}`
|
|
8
|
-
? `${Start}${string}`
|
|
9
|
-
: Path;
|
|
10
|
-
|
|
11
|
-
export type Parameters<Path extends string> =
|
|
12
|
-
Path extends `${string}/:${infer Segment}/${infer Rest}`
|
|
13
|
-
? { [K in Segment]: string } & Parameters<`/${Rest}`>
|
|
14
|
-
: Path extends `${string}/:${infer Segment}`
|
|
15
|
-
? { [K in Segment]: string }
|
|
16
|
-
: object;
|
|
17
|
-
|
|
18
|
-
export type GoToPageFunction<Path extends string> = (path: AbsolutePath<Path>) => void;
|
|
19
|
-
|
|
20
|
-
export interface PageComponentProps<Path extends string> {
|
|
21
|
-
parameters: Parameters<Path>,
|
|
22
|
-
}
|
|
1
|
+
import { Component, createContext, type Dispatch, type FunctionComponent, type ReactNode, type SetStateAction, useCallback, useContext, useEffect, useEffectEvent, useMemo, useState } from "react";
|
|
23
2
|
|
|
24
|
-
export interface
|
|
25
|
-
|
|
26
|
-
|
|
3
|
+
export interface IssueProps {
|
|
4
|
+
error: Error | null,
|
|
5
|
+
resetError: () => void
|
|
27
6
|
}
|
|
28
7
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
transition?: Transition
|
|
33
|
-
prefix?: string,
|
|
34
|
-
pages: Array<Page<Path>>
|
|
8
|
+
export interface Router<Path extends string, Locale> {
|
|
9
|
+
prefix?: string
|
|
10
|
+
locales?: Locale[]
|
|
11
|
+
transition?: Transition
|
|
35
12
|
fallback: FunctionComponent
|
|
36
13
|
issue: FunctionComponent<IssueProps>
|
|
14
|
+
pages: Array<Page<Path>>
|
|
37
15
|
}
|
|
38
16
|
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
path: string
|
|
17
|
+
export interface RouterProviderProps {
|
|
18
|
+
children: ReactNode
|
|
42
19
|
}
|
|
43
20
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
21
|
+
export interface RouterContextInterface<Locale> {
|
|
22
|
+
locale: Locale | null
|
|
23
|
+
prefix: string | null
|
|
24
|
+
path: string
|
|
25
|
+
setLocale: Dispatch<SetStateAction<Locale | null>>
|
|
47
26
|
}
|
|
48
27
|
|
|
28
|
+
export type NavigationDirection = "forward" | "backward";
|
|
49
29
|
|
|
50
|
-
|
|
51
|
-
return
|
|
30
|
+
function normalize(uri: string) {
|
|
31
|
+
return uri
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/\/+/g, "/")
|
|
35
|
+
.replace(/^\/+|\/$/g, "");
|
|
52
36
|
}
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
const pathParts =
|
|
56
|
-
const
|
|
38
|
+
function matchPath(path: string, pathname: string) {
|
|
39
|
+
const pathParts = normalize(path).split("/").filter(Boolean);
|
|
40
|
+
const pathnameParts = normalize(pathname).split("/").filter(Boolean);
|
|
57
41
|
|
|
58
|
-
return (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
42
|
+
return pathParts.length === pathnameParts.length && pathParts.every((pathPart, index) => {
|
|
43
|
+
return pathPart.startsWith(":") || pathPart === pathnameParts.at(index);
|
|
44
|
+
});
|
|
62
45
|
}
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
|
|
70
|
-
const routeParts = sanitizePath(route).split("/").filter(Boolean);
|
|
47
|
+
function matchParameters(path: string, pathname: string): Record<string, string> {
|
|
48
|
+
const pathParts = normalize(path).split("/").filter(Boolean);
|
|
49
|
+
const pathnameParts = normalize(pathname).split("/").filter(Boolean);
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!routePart) {
|
|
76
|
-
return parameters;
|
|
77
|
-
}
|
|
51
|
+
if (pathParts.length !== pathnameParts.length) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
78
54
|
|
|
55
|
+
return pathParts.reduce((parameters, pathPart, index) => {
|
|
79
56
|
if (!pathPart.startsWith(":")) {
|
|
80
57
|
return parameters;
|
|
81
58
|
}
|
|
82
59
|
|
|
83
60
|
return {
|
|
84
61
|
...parameters,
|
|
85
|
-
[
|
|
62
|
+
[pathPart.slice(1)]: pathnameParts.at(index) ?? ""
|
|
86
63
|
}
|
|
87
|
-
}, {}
|
|
64
|
+
}, {});
|
|
88
65
|
}
|
|
89
66
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return doesRouteMatchPath(sanitizePath(`${prefix ?? ""}/${route.path}`), sanitizePath(path));
|
|
93
|
-
});
|
|
67
|
+
export class Uri<Locale> {
|
|
68
|
+
private constructor(public readonly path: string, public readonly prefix: string | null, public readonly locale: Locale | null) { }
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
|
|
70
|
+
public static from<Locale>(uri: string, expectedPrefix?: string, expectedLocales?: Locale[]): Uri<Locale> {
|
|
71
|
+
const [prefixOrLocale, localeOrNothing, ...parts] = normalize(uri).split("/");
|
|
72
|
+
const locales = expectedLocales ?? [];
|
|
97
73
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
if (expectedPrefix && prefixOrLocale && prefixOrLocale === normalize(expectedPrefix)) {
|
|
75
|
+
const locale = locales.find(expectedLocale => expectedLocale === localeOrNothing);
|
|
76
|
+
|
|
77
|
+
if (locale) {
|
|
78
|
+
return new Uri<Locale>(
|
|
79
|
+
parts.join("/"),
|
|
80
|
+
prefixOrLocale,
|
|
81
|
+
locale,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return new Uri<Locale>(
|
|
86
|
+
[localeOrNothing, ...parts].join("/"),
|
|
87
|
+
prefixOrLocale,
|
|
88
|
+
null
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const locale = locales.find(expectedLocale => expectedLocale === prefixOrLocale);
|
|
93
|
+
|
|
94
|
+
if (locale) {
|
|
95
|
+
return new Uri<Locale>(
|
|
96
|
+
[localeOrNothing, ...parts].join("/"),
|
|
97
|
+
null,
|
|
98
|
+
locale
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new Uri<Locale>(
|
|
103
|
+
[prefixOrLocale, localeOrNothing, ...parts].join("/"),
|
|
104
|
+
null,
|
|
105
|
+
null
|
|
106
|
+
);
|
|
107
|
+
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
export type ExtractParams<Path extends string> =
|
|
111
|
+
Path extends `${string}:${infer Param}/${infer Rest}`
|
|
112
|
+
? Param | ExtractParams<Rest>
|
|
113
|
+
: Path extends `${string}:${infer Param}`
|
|
114
|
+
? Param
|
|
115
|
+
: never
|
|
116
|
+
|
|
117
|
+
export type Params<Path extends string> = {
|
|
118
|
+
[Key in ExtractParams<Path>]: string
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
export interface
|
|
109
|
-
|
|
121
|
+
export interface PageParams<Path extends string> {
|
|
122
|
+
parameters: Params<Path>
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
export
|
|
113
|
-
|
|
125
|
+
export interface Page<Path extends string> {
|
|
126
|
+
path: Path
|
|
127
|
+
element: FunctionComponent<PageParams<Path>>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ErrorBoundaryProps {
|
|
131
|
+
children: ReactNode;
|
|
132
|
+
issue: FunctionComponent<IssueProps>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, IssueProps> {
|
|
136
|
+
public constructor(props: ErrorBoundaryProps) {
|
|
114
137
|
super(props);
|
|
115
138
|
|
|
116
|
-
this.state = {
|
|
139
|
+
this.state = {
|
|
140
|
+
error: null,
|
|
141
|
+
resetError: this.resetError.bind(this)
|
|
142
|
+
};
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
private resetError() {
|
|
146
|
+
this.setState({ error: null });
|
|
147
|
+
}
|
|
121
148
|
|
|
122
|
-
|
|
149
|
+
public static getDerivedStateFromError(error: Error) {
|
|
150
|
+
return {
|
|
151
|
+
error
|
|
152
|
+
};
|
|
123
153
|
}
|
|
124
154
|
|
|
125
|
-
public override
|
|
126
|
-
|
|
155
|
+
public override componentDidCatch(error: unknown) {
|
|
156
|
+
this.setState({
|
|
157
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
158
|
+
});
|
|
159
|
+
}
|
|
127
160
|
|
|
161
|
+
public override render() {
|
|
128
162
|
if (this.state.error) {
|
|
129
|
-
const
|
|
130
|
-
if (this.props.transition && viewTransitionSupported) {
|
|
131
|
-
document.startViewTransition(() => {
|
|
132
|
-
this.setState({
|
|
133
|
-
error: null
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
this.setState({
|
|
141
|
-
error: null
|
|
142
|
-
});
|
|
143
|
-
}
|
|
163
|
+
const Issue = this.props.issue;
|
|
144
164
|
|
|
145
|
-
return this.
|
|
146
|
-
error: this.state.error,
|
|
147
|
-
reset
|
|
148
|
-
});
|
|
165
|
+
return <Issue error={this.state.error} resetError={this.state.resetError} />;
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
return this.props.children;
|
|
152
169
|
}
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
export
|
|
156
|
-
return
|
|
172
|
+
export function createPage<Path extends string>(page: Page<Path>): Page<Path> {
|
|
173
|
+
return {
|
|
174
|
+
...page,
|
|
175
|
+
path: normalize(page.path) as Path
|
|
176
|
+
};
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
export
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
export type Transition = (direction: "forward" | "backward", next: () => void) => void;
|
|
180
|
+
|
|
181
|
+
export const scaleFadeTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
182
|
+
try {
|
|
183
|
+
const transition = document.startViewTransition(() => {
|
|
184
|
+
next();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await transition.ready;
|
|
188
|
+
|
|
189
|
+
document.documentElement.animate(
|
|
190
|
+
[
|
|
191
|
+
{
|
|
192
|
+
transform: 'scale(1)',
|
|
193
|
+
opacity: 1
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
transform: direction === "forward" ? "scale(1.04)" : "scale(0.96)",
|
|
197
|
+
opacity: 0
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
{
|
|
201
|
+
duration: 200,
|
|
202
|
+
easing: "ease-in-out",
|
|
203
|
+
fill: "both",
|
|
204
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
document.documentElement.animate(
|
|
209
|
+
[
|
|
210
|
+
{
|
|
211
|
+
transform: direction === "forward" ? "scale(0.96)" : "scale(1.04)",
|
|
212
|
+
opacity: 0
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
transform: 'scale(1)',
|
|
216
|
+
opacity: 1
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
{
|
|
220
|
+
duration: 200,
|
|
221
|
+
easing: "ease-in-out",
|
|
222
|
+
fill: "both",
|
|
223
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error(error);
|
|
228
|
+
}
|
|
167
229
|
}
|
|
168
230
|
|
|
169
|
-
export
|
|
170
|
-
|
|
231
|
+
export const crossFadeTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
232
|
+
try {
|
|
233
|
+
const transition = document.startViewTransition(() => {
|
|
234
|
+
next();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await transition.ready;
|
|
238
|
+
|
|
239
|
+
document.documentElement.animate(
|
|
240
|
+
[
|
|
241
|
+
{ opacity: 1 },
|
|
242
|
+
{ opacity: 0 }
|
|
243
|
+
],
|
|
244
|
+
{
|
|
245
|
+
duration: 200,
|
|
246
|
+
easing: "ease-in-out",
|
|
247
|
+
fill: "both",
|
|
248
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
document.documentElement.animate(
|
|
253
|
+
[
|
|
254
|
+
{ opacity: 0 },
|
|
255
|
+
{ opacity: 1 }
|
|
256
|
+
],
|
|
257
|
+
{
|
|
258
|
+
duration: 200,
|
|
259
|
+
easing: "ease-in-out",
|
|
260
|
+
fill: "both",
|
|
261
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const slideHorizontalTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
270
|
+
try {
|
|
271
|
+
const transition = document.startViewTransition(() => {
|
|
272
|
+
next();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await transition.ready;
|
|
276
|
+
|
|
277
|
+
document.documentElement.animate(
|
|
278
|
+
[
|
|
279
|
+
{ transform: 'translateX(0)' },
|
|
280
|
+
{ transform: direction === "forward" ? 'translateX(-100%)' : 'translateX(100%)' }
|
|
281
|
+
],
|
|
282
|
+
{
|
|
283
|
+
duration: 200,
|
|
284
|
+
easing: "ease-in-out",
|
|
285
|
+
fill: "both",
|
|
286
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
document.documentElement.animate(
|
|
291
|
+
[
|
|
292
|
+
{ transform: direction === "forward" ? 'translateX(100%)' : 'translateX(-100%)' },
|
|
293
|
+
{ transform: 'translateX(0)' }
|
|
294
|
+
],
|
|
295
|
+
{
|
|
296
|
+
duration: 200,
|
|
297
|
+
easing: "ease-in-out",
|
|
298
|
+
fill: "both",
|
|
299
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(error);
|
|
304
|
+
}
|
|
171
305
|
}
|
|
172
306
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
307
|
+
export const slideVerticalTransition: Transition = async (direction: NavigationDirection, next) => {
|
|
308
|
+
try {
|
|
309
|
+
const transition = document.startViewTransition(() => {
|
|
310
|
+
next();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await transition.ready;
|
|
314
|
+
|
|
315
|
+
document.documentElement.animate(
|
|
316
|
+
[
|
|
317
|
+
{ transform: 'translateY(0)' },
|
|
318
|
+
{ transform: direction === "forward" ? 'translateY(-100%)' : 'translateY(100%)' }
|
|
319
|
+
],
|
|
320
|
+
{
|
|
321
|
+
duration: 200,
|
|
322
|
+
easing: "ease-in-out",
|
|
323
|
+
fill: "both",
|
|
324
|
+
pseudoElement: `::view-transition-old(root)`,
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
document.documentElement.animate(
|
|
329
|
+
[
|
|
330
|
+
{ transform: direction === "forward" ? 'translateY(100%)' : 'translateY(-100%)' },
|
|
331
|
+
{ transform: 'translateY(0)' }
|
|
332
|
+
],
|
|
333
|
+
{
|
|
334
|
+
duration: 200,
|
|
335
|
+
easing: "ease-in-out",
|
|
336
|
+
fill: "both",
|
|
337
|
+
pseudoElement: `::view-transition-new(root)`,
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error(error);
|
|
342
|
+
}
|
|
186
343
|
}
|
|
187
344
|
|
|
188
|
-
export
|
|
189
|
-
const
|
|
345
|
+
export function createRouter<Locale extends string = never, Path extends string = never>({ prefix: expectedPrefix, locales, pages, fallback: Fallback, issue: Issue, transition }: Router<Path, Locale>) {
|
|
346
|
+
const RouterContext = createContext<RouterContextInterface<Locale>>({
|
|
347
|
+
locale: null,
|
|
348
|
+
prefix: null,
|
|
349
|
+
path: "/",
|
|
350
|
+
setLocale: () => { },
|
|
351
|
+
});
|
|
352
|
+
|
|
190
353
|
|
|
191
|
-
|
|
192
|
-
const
|
|
354
|
+
function useIsActivePage<P extends Path>(page: Page<P>): boolean {
|
|
355
|
+
const { path } = usePath();
|
|
356
|
+
return matchPath(page.path, path);
|
|
357
|
+
}
|
|
193
358
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}, initialPath);
|
|
359
|
+
function useLocale() {
|
|
360
|
+
const context = useContext(RouterContext);
|
|
197
361
|
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
} else {
|
|
201
|
-
window.history.pushState(null, pathWithParameters, pathWithParameters);
|
|
362
|
+
if (!context) {
|
|
363
|
+
throw new Error("component using the useLocale hook has not been wrapped inside RouterProvider.");
|
|
202
364
|
}
|
|
203
365
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
366
|
+
return {
|
|
367
|
+
locale: context.locale,
|
|
368
|
+
setLocale: (locale: Locale) => {
|
|
369
|
+
if (!locales?.includes(locale)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
207
372
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
373
|
+
window.history.pushState(null, "", `/${normalize(`${context.prefix ?? ""}/${locale}/${context.path}`)}`);
|
|
374
|
+
window.dispatchEvent(new Event("pushstate"));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
213
378
|
|
|
214
|
-
|
|
215
|
-
|
|
379
|
+
function usePrefix() {
|
|
380
|
+
const context = useContext(RouterContext);
|
|
216
381
|
|
|
217
|
-
|
|
218
|
-
|
|
382
|
+
if (!context) {
|
|
383
|
+
throw new Error("Component using the usePrefix hook has not been wrapped inside RouterProvider");
|
|
384
|
+
}
|
|
219
385
|
|
|
220
|
-
|
|
221
|
-
|
|
386
|
+
return {
|
|
387
|
+
prefix: context.prefix
|
|
388
|
+
};
|
|
389
|
+
}
|
|
222
390
|
|
|
223
|
-
|
|
224
|
-
|
|
391
|
+
function usePath() {
|
|
392
|
+
const context = useContext(RouterContext);
|
|
225
393
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
394
|
+
if (!context) {
|
|
395
|
+
throw new Error("Component using the usePath hook has not been wrapped inside RouterProvider");
|
|
396
|
+
}
|
|
230
397
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
398
|
+
return {
|
|
399
|
+
path: context.path,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
235
402
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}, sanitizePath(`${prefix ?? ""}/${page.path}`));
|
|
240
|
-
}, [prefix, page, parameters]);
|
|
403
|
+
function useNavigateToPage<P extends Path>(page: Page<P>) {
|
|
404
|
+
const { locale } = useLocale();
|
|
405
|
+
const { prefix } = usePrefix();
|
|
241
406
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
407
|
+
return useCallback((...[params]: ExtractParams<P> extends never ? [] : [Params<P>]) => {
|
|
408
|
+
const path = Object.entries(params ?? {}).reduce<string>((oldParams, [name, value]) => {
|
|
409
|
+
return oldParams.replace(`:${name}`, String(value));
|
|
410
|
+
}, page.path);
|
|
246
411
|
|
|
247
|
-
|
|
248
|
-
<a
|
|
249
|
-
href={pathWithParameters}
|
|
250
|
-
onClick={navigate}>
|
|
251
|
-
{children}
|
|
252
|
-
</a>
|
|
253
|
-
);
|
|
412
|
+
const pathname = `/${normalize(`${prefix ?? ""}/${locale ?? ""}/${path}`)}`
|
|
254
413
|
|
|
255
|
-
|
|
414
|
+
console.log({ pathname });
|
|
256
415
|
|
|
257
|
-
|
|
258
|
-
|
|
416
|
+
window.history.pushState(null, "", pathname);
|
|
417
|
+
window.dispatchEvent(new Event("pushstate"));
|
|
418
|
+
}, [page, locale, prefix]);
|
|
419
|
+
}
|
|
259
420
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
421
|
+
function RouterProvider({ children }: RouterProviderProps) {
|
|
422
|
+
const uri = useMemo(() => Uri.from(window.location.pathname, expectedPrefix, locales), [expectedPrefix, locales]);
|
|
423
|
+
const [locale, setLocale] = useState(uri.locale);
|
|
424
|
+
const [path, setPath] = useState(uri.path);
|
|
425
|
+
const [prefix, setPrefix] = useState(uri.prefix);
|
|
264
426
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
document.documentElement.animate(
|
|
268
|
-
[
|
|
269
|
-
{ transform: 'translateX(0)', opacity: 1 },
|
|
270
|
-
{ transform: `translateX(${direction === NavigationDirection.Forward ? '100%' : '-100%'})`, opacity: 0 }
|
|
271
|
-
],
|
|
272
|
-
{
|
|
273
|
-
duration: 250,
|
|
274
|
-
easing: "ease-in-out",
|
|
275
|
-
fill: "both",
|
|
276
|
-
pseudoElement: "::view-transition-old(root)",
|
|
277
|
-
}
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
document.documentElement.animate(
|
|
281
|
-
[
|
|
282
|
-
{ transform: `translateX(${direction === NavigationDirection.Forward ? '-100%' : '100%'})`, opacity: 0 },
|
|
283
|
-
{ transform: 'translateX(0)', opacity: 1 }
|
|
284
|
-
],
|
|
285
|
-
{
|
|
286
|
-
duration: 250,
|
|
287
|
-
easing: "ease-in-out",
|
|
288
|
-
fill: "both",
|
|
289
|
-
pseudoElement: "::view-transition-new(root)",
|
|
427
|
+
function setHash(newHash: string) {
|
|
428
|
+
window.location.hash = newHash;
|
|
290
429
|
}
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export const createRouter = <Path extends string>({ pages, fallback, transition, issue, prefix }: CreateRouterOptions<Path>) => {
|
|
295
|
-
const Provider = ({ children }: ProviderProps) => {
|
|
296
|
-
const [pathname, setPathname] = useState(sanitizePath(window.location.pathname));
|
|
297
|
-
const [search, setSearch] = useState(new URLSearchParams(sanitizePath(window.location.search)));
|
|
298
|
-
const [hash, setHash] = useState(window.location.hash);
|
|
299
430
|
|
|
300
431
|
const value = useMemo(() => {
|
|
301
432
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
setSearch,
|
|
308
|
-
setHash
|
|
433
|
+
locale,
|
|
434
|
+
path,
|
|
435
|
+
prefix: prefix ?? null,
|
|
436
|
+
setLocale,
|
|
437
|
+
setHash,
|
|
309
438
|
};
|
|
310
|
-
}, [
|
|
439
|
+
}, [locale, prefix, path]);
|
|
311
440
|
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
transition(direction, () => {
|
|
316
|
-
setPathname(sanitizePath(window.location.pathname));
|
|
317
|
-
});
|
|
441
|
+
const onNavigation = useEffectEvent((direction: NavigationDirection) => {
|
|
442
|
+
const pathname = normalize(window.location.pathname);
|
|
443
|
+
const uri = Uri.from(pathname, expectedPrefix, locales);
|
|
318
444
|
|
|
319
|
-
|
|
320
|
-
|
|
445
|
+
const defaultTransition: Transition = (_, next) => {
|
|
446
|
+
next();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const currentTransition: Transition = transition ?? defaultTransition;
|
|
450
|
+
|
|
451
|
+
currentTransition(direction, () => {
|
|
452
|
+
setLocale(uri.locale);
|
|
453
|
+
setPath(uri.path);
|
|
454
|
+
setPrefix(uri.prefix);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const onMount = useEffectEvent(() => {
|
|
459
|
+
const pathname = `/${normalize(window.location.pathname)}`;
|
|
460
|
+
const uri = Uri.from(pathname, expectedPrefix, [locale, ...locales ?? []]);
|
|
461
|
+
const newPathname = `/${normalize(`${uri.prefix ?? expectedPrefix ?? ""}/${uri.locale ?? locales?.at(0) ?? ""}/${uri.path}`)}`;
|
|
321
462
|
|
|
322
|
-
|
|
463
|
+
if (newPathname !== pathname) {
|
|
464
|
+
window.history.pushState(null, "", newPathname);
|
|
465
|
+
window.dispatchEvent(new Event("pushstate"));
|
|
323
466
|
}
|
|
467
|
+
});
|
|
324
468
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
};
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const abortController = new AbortController();
|
|
328
471
|
|
|
329
|
-
|
|
330
|
-
onNavigation(
|
|
331
|
-
};
|
|
472
|
+
window.addEventListener("pushstate", () => {
|
|
473
|
+
onNavigation("forward");
|
|
474
|
+
}, abortController);
|
|
475
|
+
|
|
476
|
+
window.addEventListener("popstate", () => {
|
|
477
|
+
onNavigation("backward");
|
|
478
|
+
}, abortController);
|
|
332
479
|
|
|
333
|
-
|
|
334
|
-
window.addEventListener(NavigationDirection.Backward, onNavigationBackward);
|
|
480
|
+
onMount();
|
|
335
481
|
|
|
336
482
|
return () => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
483
|
+
abortController.abort();
|
|
484
|
+
};
|
|
340
485
|
}, []);
|
|
341
486
|
|
|
342
487
|
return (
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
</ErrorBoundary>
|
|
347
|
-
</Context.Provider>
|
|
488
|
+
<RouterContext.Provider value={value}>
|
|
489
|
+
{children}
|
|
490
|
+
</RouterContext.Provider>
|
|
348
491
|
);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const View = () => {
|
|
352
|
-
const Fallback = useMemo(() => fallback, []);
|
|
353
|
-
const { pathname } = useContext(Context);
|
|
354
|
-
const page = useMemo(() => findPage(pages, pathname, prefix), [pathname]);
|
|
492
|
+
}
|
|
355
493
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return getParameters(sanitizePath(page.path), sanitizePath(window.location.pathname), prefix);
|
|
359
|
-
}
|
|
494
|
+
function RouterView() {
|
|
495
|
+
const { path } = usePath();
|
|
360
496
|
|
|
361
|
-
|
|
362
|
-
|
|
497
|
+
const foundPage = useMemo(() => {
|
|
498
|
+
return pages.find(page => {
|
|
499
|
+
return matchPath(page.path, path);
|
|
500
|
+
});
|
|
501
|
+
}, [path, pages]);
|
|
363
502
|
|
|
364
|
-
if (
|
|
503
|
+
if (foundPage) {
|
|
365
504
|
return (
|
|
366
|
-
<
|
|
367
|
-
<
|
|
368
|
-
</
|
|
505
|
+
<ErrorBoundary issue={Issue}>
|
|
506
|
+
<foundPage.element parameters={matchParameters(foundPage.path, path) as Params<Path>} />
|
|
507
|
+
</ErrorBoundary>
|
|
369
508
|
);
|
|
370
509
|
}
|
|
371
510
|
|
|
372
511
|
return (
|
|
373
|
-
<
|
|
512
|
+
<ErrorBoundary issue={Issue}>
|
|
513
|
+
<Fallback />
|
|
514
|
+
</ErrorBoundary>
|
|
374
515
|
);
|
|
375
|
-
}
|
|
516
|
+
}
|
|
376
517
|
|
|
377
518
|
return {
|
|
378
|
-
|
|
379
|
-
|
|
519
|
+
RouterProvider,
|
|
520
|
+
RouterView,
|
|
521
|
+
useLocale,
|
|
522
|
+
usePrefix,
|
|
523
|
+
usePath,
|
|
524
|
+
useNavigateToPage,
|
|
525
|
+
useIsActivePage,
|
|
380
526
|
};
|
|
381
527
|
}
|