Skip to content

Commit c6bd6c6

Browse files
committed
Added 404 Fallback Functionality
1 parent c000c78 commit c6bd6c6

File tree

11 files changed

+185
-70
lines changed

11 files changed

+185
-70
lines changed

example/src/App.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import React from 'react';
1+
import React, {Suspense} from 'react';
22
import {Router, Stack, AnimationConfig} from 'react-motion-router';
3+
import { matchRoute } from 'react-motion-router/common/utils';
34
import Home from './Screens/Home';
45
import Details from './Screens/Details';
56
import Tiles from './Screens/Tiles';
67
import Slides from './Screens/Slides';
7-
import Cards from './Screens/Cards';
8+
import NotFound from './Screens/NotFound';
89
import { getPWADisplayMode, iOS } from './common/utils';
910
import "./css/App.css";
1011

12+
const Cards = React.lazy(() => import('./Screens/Cards'));
13+
1114
const isPWA = getPWADisplayMode() === 'standalone';
1215
let animation: AnimationConfig = {
1316
type: "slide",
@@ -33,10 +36,10 @@ if (iOS() && !isPWA) {
3336

3437
function App() {
3538
return (
36-
<div className="App">
39+
<Suspense fallback={<div className='app'></div>}>
3740
<Router config={{
3841
defaultRoute: '/',
39-
disableDiscovery: false,
42+
disableDiscovery: !isPWA,
4043
disableBrowserRouting: isPWA && iOS(),
4144
animation: animation
4245
}}>
@@ -62,21 +65,22 @@ function App() {
6265
component={Home}
6366
/>
6467
<Stack.Screen
65-
path={/tiles/}
68+
path={/^\/tiles/}
6669
component={Tiles}
6770
defaultParams={{params: "data"}}
6871
config={{
6972
animation: (currentPath, nextPath) => {
70-
if ((currentPath === "/tiles" && nextPath === "/slides")
71-
|| (currentPath === "/slides" && nextPath === "/tiles")) {
73+
if ((matchRoute(currentPath, "/tiles") && matchRoute(nextPath, "/slides"))
74+
|| (matchRoute(currentPath, "/slides") && matchRoute(nextPath, "/tiles"))) {
7275
return fadeAnimation;
7376
}
7477
return animation;
7578
}
7679
}}
7780
/>
81+
<Stack.Screen component={NotFound} />
7882
</Router>
79-
</div>
83+
</Suspense>
8084
);
8185
}
8286

example/src/Screens/NotFound.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react';
2+
import { Navigation } from 'react-motion-router';
3+
import BoredSaitama from '../assets/bored-saitama.gif';
4+
import Button from '@mui/material/Button';
5+
import '../css/NotFound.css';
6+
7+
interface NotFoundProps {
8+
navigation: Navigation
9+
}
10+
11+
export default function NotFound(props: NotFoundProps) {
12+
const imgWidth = window.screen.width > 400 ? 400 : window.screen.width;
13+
return (
14+
<div className="not-found">
15+
<div className="page-content">
16+
<h1>404 Not Found</h1>
17+
<img src={BoredSaitama} alt="gif" style={{width: imgWidth, height: imgWidth / 1.16}} />
18+
<Button variant="contained" onClick={() => props.navigation.navigate('/')}>Home</Button>
19+
</div>
20+
</div>
21+
);
22+
}
4.29 MB
Loading

example/src/css/App.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,17 @@
1010
body {
1111
overscroll-behavior: none;
1212
}
13+
div.app {
14+
width: 100vw;
15+
height: 100vh;
16+
}
17+
div.app::before {
18+
content: "a";
19+
color: transparent;
20+
position: absolute;
21+
top: 0;
22+
left: 0;
23+
width: 100vw;
24+
height: 64px;
25+
background-color: rgba(254, 226, 85);
26+
}

example/src/css/NotFound.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.not-found {
2+
width: 100vw;
3+
height: 100vh;
4+
background-color: #efefef;
5+
display: flex;
6+
flex-direction: column;
7+
}
8+
.page-content {
9+
flex: 1;
10+
display: flex;
11+
flex-direction: column;
12+
justify-content: center;
13+
align-items: center;
14+
gap: 15px;
15+
}
16+
.page-content button {
17+
color: rgb(214 179 111);
18+
}
19+
.page-content h1 {
20+
animation: blink 2s ease alternate infinite;
21+
}
22+
23+
@keyframes blink {
24+
from {
25+
opacity: 0;
26+
}
27+
to {
28+
opacity: 1;
29+
}
30+
}

src/AnimationLayer.tsx

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { createContext } from 'react';
22
import {SwipeEndEvent, SwipeEvent, SwipeStartEvent} from 'web-gesture-events';
3-
import { clamp, Navigation, testRoute } from './common/utils';
3+
import { clamp, Navigation, matchRoute, includesRoute } from './common/utils';
44
import {ScreenChild} from './index';
55
import AnimationLayerData, {AnimationLayerDataContext} from './AnimationLayerData';
66

@@ -30,6 +30,7 @@ interface AnimationLayerState {
3030
gestureNavigating: boolean;
3131
shouldAnimate: boolean;
3232
startX: number;
33+
paths: (string | RegExp | undefined)[]
3334
}
3435

3536
interface MotionProgressEventDetail {
@@ -52,7 +53,8 @@ export default class AnimationLayer extends React.Component<AnimationLayerProps,
5253
shouldPlay: true,
5354
gestureNavigating: false,
5455
shouldAnimate: true,
55-
startX: 0
56+
startX: 0,
57+
paths: []
5658
}
5759

5860
static getDerivedStateFromProps(nextProps: AnimationLayerProps, state: AnimationLayerState) {
@@ -64,28 +66,45 @@ export default class AnimationLayer extends React.Component<AnimationLayerProps,
6466
};
6567
}
6668

69+
const paths = [...state.paths];
70+
let nextPath: string | undefined = nextProps.currentPath;
71+
let currentPath: string | undefined = state.currentPath; // === '' when AnimationLayer first mounts
72+
let nextMatched = false;
73+
let currentMatched = false;
6774
let children = React.Children.map(
6875
nextProps.children,
6976
(child: ScreenChild) => {
7077
if (React.isValidElement(child)) {
71-
if (testRoute(child.props.path, nextProps.currentPath)) {
72-
const element = React.cloneElement(child, {...child.props, in: true, out: false});
73-
return element;
74-
} else if (testRoute(child.props.path, state.currentPath)) {
75-
const element = React.cloneElement(child, {...child.props, out: true, in: false});
76-
return element;
77-
} else if (!child.props.path) {
78-
const element = React.cloneElement(child, {...child.props, out: false, in: false});
79-
return element;
80-
} else {
81-
return undefined;
78+
if (!state.paths.length) paths.push(child.props.path);
79+
80+
if (state.paths.length) {
81+
if (!includesRoute(nextPath, paths) && state.paths.includes(undefined)) {
82+
nextPath = undefined;
83+
}
84+
if (currentPath !== '' && !includesRoute(currentPath, paths) && state.paths.includes(undefined)) {
85+
currentPath = undefined;
86+
}
87+
}
88+
89+
if (matchRoute(child.props.path, nextPath)) {
90+
if (!nextMatched) {
91+
nextMatched = true;
92+
return React.cloneElement(child, {...child.props, in: true, out: false});
93+
}
94+
}
95+
if (matchRoute(child.props.path, currentPath)) {
96+
if (!currentMatched) {
97+
currentMatched = true;
98+
return React.cloneElement(child, {...child.props, out: true, in: false});
99+
}
82100
}
83101
}
84102
}
85-
).sort((child, _) => testRoute(child.props.path, nextProps.currentPath) ? 1 : -1)
103+
).sort((child, _) => matchRoute(child.props.path, nextPath) ? 1 : -1); // current screen mounts first
86104

87105
return {
88-
children: children, // current screen mounts first
106+
paths: paths,
107+
children: children,
89108
currentPath: nextProps.currentPath
90109
}
91110
}
@@ -115,13 +134,20 @@ export default class AnimationLayer extends React.Component<AnimationLayerProps,
115134
}
116135

117136
componentDidUpdate(prevProps: AnimationLayerProps) {
137+
if (!React.Children.count(this.state.children)) {
138+
const children = React.Children.map(this.props.children, (child: ScreenChild) => {
139+
if (!React.isValidElement(child)) return undefined;
140+
if (matchRoute(child.props.path, undefined))
141+
return React.cloneElement(child, {...child.props, in: true, out: false}) as ScreenChild;
142+
});
143+
this.setState({children: children});
144+
}
118145
if (prevProps.currentPath !== this.state.currentPath) {
119146
this.animationLayerData.duration = this.props.duration;
120147
if (!this.state.gestureNavigating) {
121148
this.animationLayerData.play = true;
122149
this.animationLayerData.animate(); // children changes committed now animate
123150
}
124-
125151
}
126152
}
127153

@@ -136,27 +162,39 @@ export default class AnimationLayer extends React.Component<AnimationLayerProps,
136162
if (!this.props.lastPath) return;
137163

138164
if (ev.direction === "right" && ev.x < this.props.swipeAreaWidth) {
165+
let currentPath: string | undefined = this.props.currentPath;
166+
let lastPath: string | undefined = this.props.lastPath;
167+
let currentMatched = false;
168+
let lastMatched = false;
139169
const children = React.Children.map(
140170
this.props.children,
141171
(child: ScreenChild) => {
142172
if (!this.props.lastPath) return undefined;
143-
173+
174+
if (!includesRoute(currentPath, this.state.paths) && this.state.paths.includes(undefined)) {
175+
currentPath = undefined;
176+
}
177+
if (!includesRoute(lastPath, this.state.paths) && this.state.paths.includes(undefined)) {
178+
lastPath = undefined;
179+
}
144180
if (React.isValidElement(child)) {
145-
if (testRoute(child.props.path, this.props.currentPath)
146-
|| testRoute(child.props.path, this.props.lastPath)) {
147-
const _in = testRoute(child.props.path, this.props.currentPath) ? true : false;
148-
const element = React.cloneElement(child, {
149-
...child.props,
150-
in: _in,
151-
out: !_in
152-
});
153-
return element as ScreenChild;
181+
if (matchRoute(child.props.path, currentPath)) {
182+
if (!currentMatched) {
183+
currentMatched = true;
184+
const element = React.cloneElement(child, {...child.props, in: true, out: false});
185+
return element as ScreenChild;
186+
}
187+
}
188+
if (matchRoute(child.props.path, lastPath)) {
189+
if (!lastMatched) {
190+
lastMatched = true;
191+
const element = React.cloneElement(child, {...child.props, in: false, out: true});
192+
return element as ScreenChild;
193+
}
154194
}
155-
} else {
156-
return undefined;
157195
}
158196
}
159-
).sort((firstChild) => testRoute(firstChild.props.path, this.props.currentPath) ? -1 : 1);
197+
).sort((firstChild) => matchRoute(firstChild.props.path, currentPath) ? -1 : 1);
160198

161199
this.props.onGestureNavigationStart();
162200
this.setState({

0 commit comments

Comments
 (0)