Commit 0a1aa217 authored by Ben Galloway's avatar Ben Galloway

Initial commit based on GSC Today

parents
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# GSC Boilerplate App
This is a boilerplate web app suitable for static hosting, themed with GSC branding. It may be used as a basis to create standalone apps or apps which connect to a backend of some kind. The tools to connect to a GraphQL backend are included.
## Technologies
This boilerplate was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). The capability to install it as a [Progressive Web App (PWA)](https://developers.google.com/web/progressive-web-apps) has been enabled.
For UI, [React](https://reactjs.org/) and [Material-UI](https://material-ui.com/) are installed with [GSC theming](https://gitlab.gsc.org.uk/gsc-frameworks/gsc-material). [Redux](https://redux.js.org/style-guide/style-guide/) is used as a data layer for local state (along with [redux-persist](https://github.com/rt2zz/redux-persist) and [localforage](https://github.com/localForage/localForage) for persistence).
[React Router](https://reacttraining.com/react-router/web/guides/quick-start) provides routing capabilities, and [connected-react-router](https://github.com/supasate/connected-react-router) is used to store routing state in Redux. This is not essential so can be removed depending on developer preference.
[Apollo Client](https://www.apollographql.com/docs/react/) is installed for fetching and caching remote data from GraphQL servers, although no examples are included.
## Development
Clone the repo and install the required modules with e.g. `yarn install`.
Run `yarn start` in the project directory to open a live-reloading local version accessible at `http://localhost:3000`.
To build for a production deployment, run `yarn build`. Some small utilities to make deploying on [Netlify](https://www.netlify.com/) easier are also included.
## Troubleshooting
Happy to field any questions via [ben.galloway@gsc.org.uk](mailto:ben.galloway@gsc.org.uk).
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
\ No newline at end of file
{
"name": "gsc-boilerplate",
"version": "1.0.0",
"private": true,
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
"@gsc/material": "git+https://gitlab.gsc.org.uk/gsc-frameworks/gsc-material.git",
"@material-ui/core": "^4.7.0",
"@material-ui/icons": "^4.5.1",
"apollo-boost": "^0.4.4",
"connected-react-router": "^6.6.0",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"localforage": "^1.7.3",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
"redux": "^4.0.4",
"redux-persist": "^6.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"netlify-cli": "^2.20.2"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="#2e2d44" />
<meta name="theme-color" content="#2e2d44" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/img/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/img/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/img/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/img/favicon-16x16.png" />
<title>GSC Boilerplate App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
{
"name": "GSC Boilerplate App",
"short_name": "GSC Boilerplate",
"icons": [
{
"src": "img/favicon.ico",
"sizes": "32x32",
"type": "image/x-icon"
},
{
"src": "/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/img/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#2e2d44",
"background_color": "#2e2d44",
"display": "standalone",
"start_url": "/"
}
import React from "react";
import GSCNavBar from "./components/navbar/";
import HomeRoute from "./routes/HomeRoute";
import SecondRoute from "./routes/SecondRoute";
import AnotherRoute from "./routes/AnotherRoute";
import RouteMapper from "./routes/RouteMapper";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
toolbar: theme.mixins.toolbar,
}));
const routes = [
{
path: "/home",
render: ({ match }) => <HomeRoute match={match} />,
},
{
path: "/secondpage",
render: ({ match }) => <SecondRoute match={match} />,
},
{
path: "/anotherpage",
render: ({ match }) => <AnotherRoute match={match} />,
},
];
const App = () => {
const classes = useStyles();
// There's an "extra" div in here as a simple way to make room for the NavBar
return (
<>
<GSCNavBar />
<div className={classes.toolbar} />
<RouteMapper routes={routes} defaultRoute="/home" />
</>
);
};
export default App;
import React from "react";
import { Grid, Typography, Paper } from "@material-ui/core";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
gridContainer: {
margin: theme.spacing(2, 0),
},
colourSwatch: ({ colour, isDark }) => ({
width: "5rem",
height: "5rem",
border: `1px solid ${theme.gsc.navy}`,
backgroundColor: theme.gsc[colour],
color: isDark ? theme.gsc.white : theme.gsc.navy,
display: "flex",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "flex-end",
padding: theme.spacing(0.5, 1),
margin: theme.spacing(1),
}),
}));
const ColourSwatch = ({ colour, isDark }) => {
const classes = useStyles({ colour, isDark });
return (
<Paper elevation={0} variant="outlined" square className={classes.colourSwatch}>
{colour}
</Paper>
);
};
const ColourSwatches = () => {
const classes = useStyles();
return (
<Grid
container
direction="column"
justify="flex-start"
alignItems="flex-start"
className={classes.gridContainer}
>
<Typography variant="h5" component="h3">
GSC Colour Swatches
</Typography>
<Typography variant="h6" component="h4">
Primary
</Typography>
<Grid item xs={12} container direction="row" justify="flex-start" alignItems="center">
<ColourSwatch colour="navy" isDark />
<ColourSwatch colour="teal" isDark />
<ColourSwatch colour="offwhite" />
</Grid>
<Typography variant="h6" component="h4">
Secondary
</Typography>
<Grid item xs={12} container direction="row" justify="flex-start" alignItems="center">
<ColourSwatch colour="pink" isDark />
<ColourSwatch colour="gold" isDark />
</Grid>
<Typography variant="h6" component="h4">
Tertiary
</Typography>
<Grid item xs={12} container direction="row" justify="flex-start" alignItems="center">
<ColourSwatch colour="darkteal" isDark />
<ColourSwatch colour="blue" isDark />
<ColourSwatch colour="lime" />
<ColourSwatch colour="orange" isDark />
<ColourSwatch colour="red" isDark />
<ColourSwatch colour="purple" isDark />
<ColourSwatch colour="green" isDark />
<ColourSwatch colour="white" />
</Grid>
</Grid>
);
};
export default ColourSwatches;
import React from "react";
import { Grid, Typography, Button } from "@material-ui/core";
import { useSelector, useDispatch } from "react-redux";
import { counterSelector, counterActions } from "../../redux/counterSlice";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
counterGrid: {
margin: theme.spacing(2, 0),
},
counter: {
padding: theme.spacing(0, 2),
},
}));
const Counter = () => {
// This is a simple example component to give Redux something to actually do
const classes = useStyles();
const { counter } = useSelector(counterSelector);
const dispatch = useDispatch();
return (
<Grid
item
xs={12}
container
direction="row"
justify="flex-start"
alignItems="center"
className={classes.counterGrid}
>
<Button variant="outlined" color="secondary" onClick={() => dispatch({ type: counterActions.decrement })}>
-
</Button>
<Typography variant="h5" component="h3" className={classes.counter}>
{counter}
</Typography>
<Button variant="outlined" color="secondary" onClick={() => dispatch({ type: counterActions.increment })}>
+
</Button>
</Grid>
);
};
export default Counter;
import React from "react";
import { IconButton } from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
// The use of React.forwardRef will no longer be required for react-router-dom v6.
// See https://github.com/ReactTraining/react-router/issues/6056
const Link = React.forwardRef((props, ref) => <RouterLink innerRef={ref} {...props} />);
const NavIconButton = ({ icon, to, color, label }) => {
return (
<IconButton color={color} component={Link} to={to} aria-label={label}>
{icon}
</IconButton>
);
};
export default NavIconButton;
import React, { useState, useEffect } from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import AddToHomeScreenIcon from "@material-ui/icons/AddToHomeScreen";
const listenForPromptEvent = updateDeferredPrompt => {
window.addEventListener("beforeinstallprompt", e => {
e.preventDefault();
// Stash the event so it can be triggered later.
updateDeferredPrompt(e);
});
};
const AddToHomeScreen = () => {
const [deferredPrompt, updateDeferredPrompt] = useState(null);
useEffect(() => listenForPromptEvent(updateDeferredPrompt), []);
const install = () => {
deferredPrompt.prompt();
updateDeferredPrompt(null);
};
const isRunningStandalone =
window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true;
return (
!isRunningStandalone &&
deferredPrompt && (
<ListItem button onClick={install}>
<ListItemIcon>
<AddToHomeScreenIcon />
</ListItemIcon>
<ListItemText primary="Install as an app" secondary="Add to homescreen or desktop" />
</ListItem>
)
);
};
export default AddToHomeScreen;
import React from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import { Link as RouterLink } from "react-router-dom";
const ListItemLink = ({ icon, primary, secondary, to, toggler }) => {
// See https://material-ui.com/guides/composition/#react-router
const renderLink = React.useMemo(
() => React.forwardRef((itemProps, ref) => <RouterLink to={to} {...itemProps} ref={ref} />),
[to]
);
return (
<ListItem button component={renderLink} onClick={toggler}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={primary} secondary={secondary} />
</ListItem>
);
};
export default ListItemLink;
import React from "react";
import { Drawer, List, Divider, useMediaQuery } from "@material-ui/core";
import ListItemLink from "./ListItemLink";
import AddToHomeScreen from "./AddToHomeScreen";
import HomeIcon from "@material-ui/icons/Home";
import EqualizerIcon from "@material-ui/icons/Equalizer";
import EventIcon from "@material-ui/icons/Event";
import { makeStyles, useTheme } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
drawer: ({ drawerWidth, open }) => ({
width: open ? drawerWidth : 0,
flexShrink: 0,
}),
drawerPaper: ({ drawerWidth }) => ({
width: drawerWidth,
}),
toolbar: theme.mixins.toolbar,
}));
const navItems = [
{ primary: "Home Page", secondary: "The home page", icon: <HomeIcon />, to: "/" },
{
primary: "A Second Page",
secondary: "A page that is not the home page",
icon: <EqualizerIcon />,
to: "/secondpage",
},
{
primary: "Another Page",
secondary: "Neither home nor second",
icon: <EventIcon />,
to: "/anotherpage",
},
];
const NavigationDrawer = ({ drawerWidth, open, toggle }) => {
const classes = useStyles({ drawerWidth, open });
const theme = useTheme();
const isMinSmScreen = useMediaQuery(theme.breakpoints.up("sm"));
return (
<nav className={isMinSmScreen ? classes.drawer : ""} aria-label="navigation menu">
<Drawer
variant="persistent"
anchor={theme.direction === "rtl" ? "right" : "left"}
open={open}
onClose={toggle}
classes={{
paper: classes.drawerPaper,
}}
>
<div className={classes.toolbar} />
<List>
{navItems.map(item => (
<ListItemLink {...item} key={item.primary} toggler={toggle} />
))}
<Divider />
<AddToHomeScreen />
</List>
</Drawer>
</nav>
);
};
export default NavigationDrawer;
import React from "react";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import NavigationDrawer from "./NavDrawer";
import MenuIcon from "@material-ui/icons/Menu";
import MenuOpenIcon from "@material-ui/icons/MenuOpen";
import AccountIcon from "@material-ui/icons/AccountCircle";
import { Link } from "react-router-dom";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router";
import { makeStyles } from "@material-ui/core/styles";
import { navDrawerWidth } from "../../config";
import gscLogoInverse from "../../assets/gsclogo-inverse.png";
const useStyles = makeStyles(theme => ({
appBar: {
zIndex: theme.zIndex.drawer + 1,
},
title: {
flexGrow: 1,
},
logo: {
width: "70px",
marginLeft: theme.spacing(0),
marginRight: theme.spacing(1),
[theme.breakpoints.up("md")]: {
width: "80px",
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
},
},
appLink: {
textDecoration: "none",
color: theme.gsc.offwhite,
"&:hover": {
textDecoration: "none",
color: theme.gsc.offwhite,
},
},
}));
const GSCNavBar = () => {
const classes = useStyles();
const dispatch = useDispatch();
const [navDrawerOpen, setNavDrawerOpen] = React.useState(false);
const handleDrawerToggle = () => setNavDrawerOpen(!navDrawerOpen);
return (
<>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
aria-label="open navigation menu"
onClick={handleDrawerToggle}
>
{navDrawerOpen ? <MenuOpenIcon /> : <MenuIcon />}
</IconButton>
<Link to="/" className={classes.appLink}>
<img className={classes.logo} src={gscLogoInverse} alt="" />
</Link>
<Link to="/" className={`${classes.appLink} ${classes.title}`}>
<Typography variant="h4" component="h1">
Boilerplate App
</Typography>
</Link>
<IconButton
edge="end"
color="inherit"
aria-label="navigate to anotherpage"
onClick={() => dispatch(push("/anotherpage"))}
>
<AccountIcon />
</IconButton>
</Toolbar>
</AppBar>
<NavigationDrawer drawerWidth={navDrawerWidth} open={navDrawerOpen} toggle={handleDrawerToggle} />
</>
);
};
export default GSCNavBar;
import React from "react";
import { Container, Grid, Typography, Button } from "@material-ui/core";
import ColourSwatches from "../general/ColourSwatches";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
gridContainer: {
margin: theme.spacing(2, 0),
},
}));
const HomePage = () => {
const classes = useStyles();
const dispatch = useDispatch();
return (
<Container maxWidth="xl" className={classes.gridContainer}>
<Grid container direction="column" justify="center" alignItems="flex-start">
<Typography variant="h4" component="h2">
Home Page
</Typography>
<Typography variant="body1">
This is a boilerplate app with GSC theming on Material-UI. See the README for details.
</Typography>
<ColourSwatches />
<Button variant="contained" color="secondary" onClick={() => dispatch(push("/secondpage"))}>
Navigate to Second Page
</Button>
</Grid>
</Container>
);
};
export default HomePage;
import React from "react";
import { Container, Grid, Typography, Button } from "@material-ui/core";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
gridContainer: {
margin: theme.spacing(2, 0),
},
}));
const ItemPage = ({ item }) => {
const classes = useStyles();
const dispatch = useDispatch();
return (
<Container maxWidth="xl" className={classes.gridContainer}>
<Grid container direction="column" justify="center" alignItems="flex-start">
<Typography variant="h4" component="h2">
Item Page
</Typography>
<Typography variant="body1">This is item {item}</Typography>
<Button variant="contained" color="primary" onClick={() => dispatch(push("/anotherpage"))}>
Back to List
</Button>
</Grid>
</Container>
);
};
export default ItemPage;
import React from "react";
import { Container, Grid, Typography, Button } from "@material-ui/core";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
gridContainer: {
margin: theme.spacing(2, 0),
},
}));
const ListPage = () => {
const classes = useStyles();
const dispatch = useDispatch();
const itemIds = [1, 2, 3, 4, 5];
return (
<Container maxWidth="xl" className={classes.gridContainer}>
<Grid container direction="column" justify="center" alignItems="flex-start">
<Typography variant="h4" component="h2">
List Page
</Typography>
<Typography variant="body1">Here is a clickable list of items:</Typography>
{itemIds.map(item => (
<Button
key={item}
variant="text"
color="secondary"
onClick={() => dispatch(push(`/anotherpage/item/${item}`))}
>
Item ID {item}
</Button>
))}
</Grid>
</Container>
);
};
export default ListPage;
import React from "react";
import { Container, Grid, Typography, Button, Card } from "@material-ui/core";
import Counter from "../general/Counter";
import { useDispatch } from "react-redux";
import { push } from "connected-react-router";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
gridContainer: {
margin: theme.spacing(2, 0),
},
secondPageCard: {
margin: theme.spacing(2, 0),
padding: theme.spacing(2),
border: `1px solid ${theme.gsc.navy}`,
},
}));
const SecondPage = () => {
const classes = useStyles();
const dispatch = useDispatch();
return (
<Container maxWidth="xl" className={classes.gridContainer}>
<Grid container direction="column" justify="center" alignItems="flex-start">
<Typography variant="h4" component="h2">
Second Page
</Typography>
<Card variant="outlined" className={classes.secondPageCard}>
<Typography variant="body1">
This content is on a card. The counter below stores its state in Redux. The state is persisted.
</Typography>
<Counter />
<Button variant="contained" color="primary" onClick={() => dispatch(push("/anotherpage"))}>
Navigate to Another Page
</Button>
</Card>
</Grid>
</Container>
);
};
export default SecondPage;
export const apolloParams = {
uri: process.env.NODE_ENV === "development" ? "http://localhost:4000" : "https://gammaql.gsc.org.uk",
};
export const navDrawerWidth = 250; //px
export const storeRootString = "gscboilerplate";
import React from "react";
import ReactDOM from "react-dom";
import GSCMaterial from "@gsc/material";
import { ApolloProvider } from "@apollo/react-hooks";
import ApolloClient from "apollo-boost";
import { apolloParams } from "./config";
import { Provider as ReduxProvider } from "react-redux";
import store, { history, persistor } from "./redux/store";
import { ConnectedRouter } from "connected-react-router";
import { PersistGate } from "redux-persist/integration/react";
import registerServiceWorker from "./registerServiceWorker";
import "@gsc/material/ProximaNovaFontFace.css";
import App from "./App";
import { CircularProgress } from "@material-ui/core";
const ConnectedApp = () => {
return (
<GSCMaterial>
<ReduxProvider store={store}>
<ApolloProvider client={new ApolloClient(apolloParams)}>
<ConnectedRouter history={history}>
<PersistGate loading={<CircularProgress color="secondary" />} persistor={persistor}>
<App />
</PersistGate>
</ConnectedRouter>
</ApolloProvider>
</ReduxProvider>
</GSCMaterial>
);
};
ReactDOM.render(<ConnectedApp />, document.getElementById("root"));
registerServiceWorker();
export const counterSelector = state => {
if (!state.counter) return { ...state, counter: 0 };
return state.counter;
};
export const counterActions = {
increment: "counter/increment",
decrement: "counter/decrement",
};
const counterReducer = (state = { counter: 0 }, action) => {
switch (action.type) {
case counterActions.increment: {
return { ...state, counter: state.counter + 1 };
}
case counterActions.decrement: {
return { ...state, counter: state.counter - 1 };
}
default:
return state;
}
};
export default counterReducer;
import { combineReducers } from "redux";
import { connectRouter } from "connected-react-router";
import counterReducer from "./counterSlice";
const createRootReducer = history =>
combineReducers({
router: connectRouter(history),
counter: counterReducer,
});
export default createRootReducer;
import { createStore, applyMiddleware, compose } from "redux";
import { persistStore, persistReducer } from "redux-persist";
import localForage from "localforage";
import { routerMiddleware } from "connected-react-router";
import { createBrowserHistory } from "history";
import createRootReducer from "./rootReducer";
import { storeRootString } from "../config";
export const history = createBrowserHistory();
const persistConfig = {
key: storeRootString,
storage: localForage,
blacklist: ["router"],
};
const rootReducer = createRootReducer(history);
const persistedReducer = persistReducer(persistConfig, rootReducer);
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(persistedReducer, composeEnhancers(applyMiddleware(routerMiddleware(history))));
export const persistor = persistStore(store);
export default store;
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
import React from "react";
import RouteMapper from "./RouteMapper";
import ListPage from "../components/root/ListPage";
import ItemPage from "../components/root/ItemPage";
const routes = [
{
path: "/item/:itemId",
render: ({ match }) => <ItemPage item={match.params.itemId} />,
},
{
path: "/list",
render: () => <ListPage />,
},
];
const AnotherRoute = ({ match }) => <RouteMapper routes={routes} rootPath={match.path} defaultRoute="/list" />;
export default AnotherRoute;
import React from "react";
import RouteMapper from "./RouteMapper";
import HomePage from "../components/root/HomePage";
const routes = [
{
path: "/",
render: () => <HomePage />,
},
];
const HomeRoute = ({ match }) => <RouteMapper routes={routes} rootPath={match.path} defaultRoute="/" />;
export default HomeRoute;
import React from "react";
import { Route, Switch, Redirect } from "react-router-dom";
const RouteMapper = ({ routes, rootPath, defaultRoute }) => {
const root = rootPath === "/" || !rootPath ? "" : rootPath;
return (
<Switch>
{routes.map(({ path, render }, index) => (
<Route key={index} path={`${root}${path}`} render={render} />
))}
<Route path={root} render={() => <Redirect to={`${root}${defaultRoute}`} />} />
</Switch>
);
};
export default RouteMapper;
import React from "react";
import RouteMapper from "./RouteMapper";
import SecondPage from "../components/root/SecondPage";
const routes = [
{
path: "/default",
render: ({ match }) => <SecondPage />,
},
];
const SecondRoute = ({ match }) => <RouteMapper routes={routes} rootPath={match.path} defaultRoute="/default" />;
export default SecondRoute;
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment