@aminnairi/react-router 2.2.0 → 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/index.tsx CHANGED
@@ -1,389 +1,527 @@
1
- /* eslint-disable */
2
- import { useEffect, useState, FunctionComponent, useMemo, Component, PropsWithChildren, createContext, SetStateAction, Dispatch, ReactNode, useContext, useCallback, memo, MouseEvent, ComponentProps, JSXElementConstructor, ElementType, ComponentPropsWithoutRef, MouseEventHandler } 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 Page<Path extends string> {
25
- path: Path,
26
- element: FunctionComponent<PageComponentProps<Path>>
3
+ export interface IssueProps {
4
+ error: Error | null,
5
+ resetError: () => void
27
6
  }
28
7
 
29
- export type Transition = (direction: NavigationDirection, next: () => void) => void;
30
-
31
- export interface CreateRouterOptions<Path extends string> {
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 FindPageOptions {
40
- pages: Array<Page<string>>
41
- path: string
17
+ export interface RouterProviderProps {
18
+ children: ReactNode
42
19
  }
43
20
 
44
- export const sanitizePath = (path: string): string => {
45
- const sanitizedPath = path.replace(/\/+/g, "/").replace(/^\/|\/$/g, "")
46
- return "/" + sanitizedPath;
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
- export const createPage = <Path extends string>(page: Page<Path>) => {
51
- return page
30
+ function normalize(uri: string) {
31
+ return uri
32
+ .trim()
33
+ .toLowerCase()
34
+ .replace(/\/+/g, "/")
35
+ .replace(/^\/+|\/$/g, "");
52
36
  }
53
37
 
54
- export const doesRouteMatchPath = (path: string, route: string, prefix?: string): boolean => {
55
- const pathParts = sanitizePath(`${prefix ?? ""}/${path}`).split("/").filter(Boolean);
56
- const routeParts = sanitizePath(route).split("/").filter(Boolean);
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
- pathParts.length === routeParts.length &&
60
- pathParts.every((part, index) => part.startsWith(":") || part === routeParts[index])
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
- export const getParameters = <Path extends string>(path: Path, route: string, prefix?: string): Parameters<Path> => {
65
- if (!doesRouteMatchPath(path, route, prefix)) {
66
- return {} as Parameters<Path>;
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
- return pathParts.reduce((parameters, pathPart, pathPartIndex) => {
73
- const routePart = routeParts[pathPartIndex];
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
- [`${pathPart.slice(1)}`]: routePart
62
+ [pathPart.slice(1)]: pathnameParts.at(index) ?? ""
86
63
  }
87
- }, {} as Parameters<Path>);
64
+ }, {});
88
65
  }
89
66
 
90
- const findPage = (pages: Array<Page<string>>, path: string, prefix?: string) => {
91
- const foundPage = pages.find(route => {
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
- return foundPage;
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
- export interface IssueProps {
99
- error: Error,
100
- reset: () => void,
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 interface ErrorBoundaryProps {
104
- fallback: FunctionComponent<IssueProps>,
105
- transition?: Transition
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 ErrorBoundaryState {
109
- error: Error | null
121
+ export interface PageParams<Path extends string> {
122
+ parameters: Params<Path>
110
123
  }
111
124
 
112
- export class ErrorBoundary extends Component<PropsWithChildren<ErrorBoundaryProps>, ErrorBoundaryState> {
113
- constructor(props: ErrorBoundaryProps) {
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 = { error: null };
139
+ this.state = {
140
+ error: null,
141
+ resetError: this.resetError.bind(this)
142
+ };
117
143
  }
118
144
 
119
- public static getDerivedStateFromError(error: unknown) {
120
- const normalizedError = error instanceof Error ? error : new Error(String(error));
145
+ private resetError() {
146
+ this.setState({ error: null });
147
+ }
121
148
 
122
- return { error: normalizedError };
149
+ public static getDerivedStateFromError(error: Error) {
150
+ return {
151
+ error
152
+ };
123
153
  }
124
154
 
125
- public override render() {
126
- const viewTransitionSupported = typeof document.startViewTransition === "function";
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 reset = () => {
130
- if (this.props.transition && viewTransitionSupported) {
131
- document.startViewTransition(() => {
132
- this.setState({
133
- error: null
134
- });
135
- });
136
-
137
- return;
138
- }
163
+ const Issue = this.props.issue;
139
164
 
140
- this.setState({
141
- error: null
142
- });
143
- }
144
-
145
- return this.props.fallback({
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 const createIssue = (issue: FunctionComponent<IssueProps>) => {
156
- return issue;
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 interface ContextInterface {
160
- prefix: string,
161
- pathname: string,
162
- setPathname: Dispatch<SetStateAction<string>>,
163
- search: URLSearchParams,
164
- setSearch: Dispatch<SetStateAction<URLSearchParams>>,
165
- hash: string,
166
- setHash: Dispatch<SetStateAction<string>>
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 interface ProviderProps {
170
- children: ReactNode
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
+ }
171
267
  }
172
268
 
173
- const Context = createContext<ContextInterface>({
174
- prefix: "",
175
- pathname: sanitizePath(window.location.pathname),
176
- setPathname: () => { },
177
- search: new URLSearchParams(),
178
- setSearch: () => { },
179
- hash: window.location.hash,
180
- setHash: () => { }
181
- });
182
-
183
- enum NavigationDirection {
184
- Forward = "pushstate",
185
- Backward = "popstate"
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
+ }
186
305
  }
187
306
 
188
- export const useNavigateToPage = <Path extends string>(page: Page<Path>) => {
189
- const { prefix } = useContext(Context);
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
+ );
190
327
 
191
- return useCallback((parameters: Parameters<Path>, replace: boolean = false) => {
192
- const initialPath = sanitizePath(`${prefix ?? ""}/${page.path}`);
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
+ }
343
+ }
193
344
 
194
- const pathWithParameters = Object.entries(parameters).reduce((path, [parameterName, parameterValue]) => {
195
- return path.replace(`:${parameterName}`, parameterValue);
196
- }, initialPath);
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
+ });
197
352
 
198
- if (replace) {
199
- window.history.replaceState(null, pathWithParameters, pathWithParameters);
200
- } else {
201
- window.history.pushState(null, pathWithParameters, pathWithParameters);
202
- }
203
353
 
204
- window.dispatchEvent(new CustomEvent(NavigationDirection.Forward));
205
- }, [page, prefix]);
206
- };
354
+ function useIsActivePage<P extends Path>(page: Page<P>): boolean {
355
+ const { path } = usePath();
356
+ return matchPath(page.path, path);
357
+ }
207
358
 
208
- export const useNavigateBack = () => {
209
- return useCallback(() => {
210
- window.dispatchEvent(new CustomEvent(NavigationDirection.Backward));
211
- }, []);
212
- }
359
+ function useLocale() {
360
+ const context = useContext(RouterContext);
213
361
 
214
- export const useIsActivePage = (page: Page<string>) => {
215
- const { pathname, prefix } = useContext(Context);
362
+ if (!context) {
363
+ throw new Error("component using the useLocale hook has not been wrapped inside RouterProvider.");
364
+ }
216
365
 
217
- return doesRouteMatchPath(sanitizePath(page.path), sanitizePath(pathname), prefix);
218
- };
366
+ return {
367
+ locale: context.locale,
368
+ setLocale: (locale: Locale) => {
369
+ if (!locales?.includes(locale)) {
370
+ return;
371
+ }
219
372
 
220
- export const useSearch = () => {
221
- const { search } = useContext(Context);
373
+ window.history.pushState(null, "", `/${normalize(`${context.prefix ?? ""}/${locale}/${context.path}`)}`);
374
+ window.dispatchEvent(new Event("pushstate"));
375
+ }
376
+ }
377
+ }
222
378
 
223
- return search;
224
- };
379
+ function usePrefix() {
380
+ const context = useContext(RouterContext);
225
381
 
226
- export const useHash = () => {
227
- const { hash } = useContext(Context);
228
- return hash;
229
- };
382
+ if (!context) {
383
+ throw new Error("Component using the usePrefix hook has not been wrapped inside RouterProvider");
384
+ }
230
385
 
231
- export type LinkProps<Path extends string> = {
232
- children: ReactNode,
233
- parameters: Parameters<Path>
234
- }
386
+ return {
387
+ prefix: context.prefix
388
+ };
389
+ }
235
390
 
236
- export type UseLinkRenderFunction = (props: { path: string, onClick: MouseEventHandler, children: ReactNode }) => ReactNode
391
+ function usePath() {
392
+ const context = useContext(RouterContext);
237
393
 
238
- export const useLink = <Path extends string>(page: Page<Path>, render?: UseLinkRenderFunction) => {
239
- const Link = memo(({ children, parameters }: LinkProps<Path>) => {
240
- const { prefix } = useContext(Context);
241
- const navigateToPage = useNavigateToPage(page);
394
+ if (!context) {
395
+ throw new Error("Component using the usePath hook has not been wrapped inside RouterProvider");
396
+ }
242
397
 
243
- const path = useMemo(() => {
244
- return Object.entries(parameters).reduce((previousPath, [parameterName, parameterValue]) => {
245
- return previousPath.replace(`:${parameterName}`, parameterValue);
246
- }, sanitizePath(`${prefix ?? ""}/${page.path}`));
247
- }, [prefix, page, parameters]);
398
+ return {
399
+ path: context.path,
400
+ };
401
+ }
248
402
 
249
- const onClick = useCallback((event: MouseEvent) => {
250
- event.preventDefault();
251
- navigateToPage(parameters);
252
- }, [navigateToPage, parameters]);
403
+ function useNavigateToPage<P extends Path>(page: Page<P>) {
404
+ const { locale } = useLocale();
405
+ const { prefix } = usePrefix();
253
406
 
254
- if (render) {
255
- return render({ path, onClick, children });
256
- }
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);
257
411
 
258
- return (
259
- <a href={path} onClick={onClick}>
260
- {children}
261
- </a>
262
- );
263
- });
412
+ const pathname = `/${normalize(`${prefix ?? ""}/${locale ?? ""}/${path}`)}`
264
413
 
265
- return Link;
266
- };
414
+ console.log({ pathname });
267
415
 
268
- export const slideFadeTransition: Transition = async (direction: NavigationDirection, next) => {
269
- const transition = document.startViewTransition(() => {
270
- next();
271
- });
416
+ window.history.pushState(null, "", pathname);
417
+ window.dispatchEvent(new Event("pushstate"));
418
+ }, [page, locale, prefix]);
419
+ }
272
420
 
273
- await transition.ready;
274
-
275
- document.documentElement.animate(
276
- [
277
- { transform: 'translateX(0)', opacity: 1 },
278
- { transform: `translateX(${direction === NavigationDirection.Forward ? '100%' : '-100%'})`, opacity: 0 }
279
- ],
280
- {
281
- duration: 250,
282
- easing: "ease-in-out",
283
- fill: "both",
284
- pseudoElement: "::view-transition-old(root)",
285
- }
286
- );
287
-
288
- document.documentElement.animate(
289
- [
290
- { transform: `translateX(${direction === NavigationDirection.Forward ? '-100%' : '100%'})`, opacity: 0 },
291
- { transform: 'translateX(0)', opacity: 1 }
292
- ],
293
- {
294
- duration: 250,
295
- easing: "ease-in-out",
296
- fill: "both",
297
- pseudoElement: "::view-transition-new(root)",
298
- }
299
- );
300
- }
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);
301
426
 
302
- export const createRouter = <Path extends string>({ pages, fallback, transition, issue, prefix }: CreateRouterOptions<Path>) => {
303
- const Provider = ({ children }: ProviderProps) => {
304
- const [pathname, setPathname] = useState(sanitizePath(window.location.pathname));
305
- const [search, setSearch] = useState(new URLSearchParams(sanitizePath(window.location.search)));
306
- const [hash, setHash] = useState(window.location.hash);
427
+ function setHash(newHash: string) {
428
+ window.location.hash = newHash;
429
+ }
307
430
 
308
431
  const value = useMemo(() => {
309
432
  return {
310
- prefix: prefix ?? "",
311
- pathname,
312
- search,
313
- hash,
314
- setPathname,
315
- setSearch,
316
- setHash
433
+ locale,
434
+ path,
435
+ prefix: prefix ?? null,
436
+ setLocale,
437
+ setHash,
317
438
  };
318
- }, [prefix, pathname, search, hash]);
439
+ }, [locale, prefix, path]);
319
440
 
320
- useEffect(() => {
321
- const onNavigation = async (direction: NavigationDirection) => {
322
- if (transition) {
323
- transition(direction, () => {
324
- setPathname(sanitizePath(window.location.pathname));
325
- });
441
+ const onNavigation = useEffectEvent((direction: NavigationDirection) => {
442
+ const pathname = normalize(window.location.pathname);
443
+ const uri = Uri.from(pathname, expectedPrefix, locales);
326
444
 
327
- return;
328
- }
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}`)}`;
329
462
 
330
- setPathname(sanitizePath(window.location.pathname));
463
+ if (newPathname !== pathname) {
464
+ window.history.pushState(null, "", newPathname);
465
+ window.dispatchEvent(new Event("pushstate"));
331
466
  }
467
+ });
332
468
 
333
- const onNavigationForward = () => {
334
- onNavigation(NavigationDirection.Forward);
335
- };
469
+ useEffect(() => {
470
+ const abortController = new AbortController();
336
471
 
337
- const onNavigationBackward = () => {
338
- onNavigation(NavigationDirection.Backward);
339
- };
472
+ window.addEventListener("pushstate", () => {
473
+ onNavigation("forward");
474
+ }, abortController);
475
+
476
+ window.addEventListener("popstate", () => {
477
+ onNavigation("backward");
478
+ }, abortController);
340
479
 
341
- window.addEventListener(NavigationDirection.Forward, onNavigationForward);
342
- window.addEventListener(NavigationDirection.Backward, onNavigationBackward);
480
+ onMount();
343
481
 
344
482
  return () => {
345
- window.removeEventListener(NavigationDirection.Forward, onNavigationForward);
346
- window.removeEventListener(NavigationDirection.Backward, onNavigationBackward);
347
- }
483
+ abortController.abort();
484
+ };
348
485
  }, []);
349
486
 
350
487
  return (
351
- <Context.Provider value={value}>
352
- <ErrorBoundary fallback={issue}>
353
- {children}
354
- </ErrorBoundary>
355
- </Context.Provider>
488
+ <RouterContext.Provider value={value}>
489
+ {children}
490
+ </RouterContext.Provider>
356
491
  );
357
- };
358
-
359
- const View = () => {
360
- const Fallback = useMemo(() => fallback, []);
361
- const { pathname } = useContext(Context);
362
- const page = useMemo(() => findPage(pages, pathname, prefix), [pathname]);
492
+ }
363
493
 
364
- const parameters = useMemo(() => {
365
- if (page) {
366
- return getParameters(sanitizePath(page.path), sanitizePath(window.location.pathname), prefix);
367
- }
494
+ function RouterView() {
495
+ const { path } = usePath();
368
496
 
369
- return {};
370
- }, [page]);
497
+ const foundPage = useMemo(() => {
498
+ return pages.find(page => {
499
+ return matchPath(page.path, path);
500
+ });
501
+ }, [path, pages]);
371
502
 
372
- if (page) {
503
+ if (foundPage) {
373
504
  return (
374
- <div >
375
- <page.element parameters={parameters} />
376
- </div>
505
+ <ErrorBoundary issue={Issue}>
506
+ <foundPage.element parameters={matchParameters(foundPage.path, path) as Params<Path>} />
507
+ </ErrorBoundary>
377
508
  );
378
509
  }
379
510
 
380
511
  return (
381
- <Fallback />
512
+ <ErrorBoundary issue={Issue}>
513
+ <Fallback />
514
+ </ErrorBoundary>
382
515
  );
383
- };
516
+ }
384
517
 
385
518
  return {
386
- View,
387
- Provider,
519
+ RouterProvider,
520
+ RouterView,
521
+ useLocale,
522
+ usePrefix,
523
+ usePath,
524
+ useNavigateToPage,
525
+ useIsActivePage,
388
526
  };
389
527
  }