Add dynamic titles to all pages via react-helmet (related to #148)
This commit is contained in:
parent
40d23c537e
commit
8f2861a8c9
40
frontend/package-lock.json
generated
40
frontend/package-lock.json
generated
|
@ -26,6 +26,7 @@
|
||||||
"proj4": "^2.7.5",
|
"proj4": "^2.7.5",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "^6.15.8",
|
"react-hook-form": "^6.15.8",
|
||||||
"react-map-gl": "^6.1.17",
|
"react-map-gl": "^6.1.17",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
|
@ -7315,6 +7316,20 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-helmet": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-fast-compare": "^3.1.1",
|
||||||
|
"react-side-effect": "^2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "6.15.8",
|
"version": "6.15.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.8.tgz",
|
||||||
|
@ -7503,6 +7518,14 @@
|
||||||
"react": ">=15"
|
"react": ">=15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-side-effect": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.3.0 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||||
|
@ -14572,6 +14595,17 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||||
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
|
||||||
},
|
},
|
||||||
|
"react-helmet": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
|
||||||
|
"requires": {
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-fast-compare": "^3.1.1",
|
||||||
|
"react-side-effect": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-hook-form": {
|
"react-hook-form": {
|
||||||
"version": "6.15.8",
|
"version": "6.15.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.8.tgz",
|
||||||
|
@ -14723,6 +14757,12 @@
|
||||||
"tiny-warning": "^1.0.0"
|
"tiny-warning": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-side-effect": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"proj4": "^2.7.5",
|
"proj4": "^2.7.5",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-helmet": "^6.1.0",
|
||||||
"react-hook-form": "^6.15.8",
|
"react-hook-form": "^6.15.8",
|
||||||
"react-map-gl": "^6.1.17",
|
"react-map-gl": "^6.1.17",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom'
|
||||||
import {useObservable} from 'rxjs-hooks'
|
import {useObservable} from 'rxjs-hooks'
|
||||||
import {from} from 'rxjs'
|
import {from} from 'rxjs'
|
||||||
import {pluck} from 'rxjs/operators'
|
import {pluck} from 'rxjs/operators'
|
||||||
|
import {Helmet} from "react-helmet";
|
||||||
|
|
||||||
import {useConfig} from 'config'
|
import {useConfig} from 'config'
|
||||||
import styles from './App.module.less'
|
import styles from './App.module.less'
|
||||||
|
@ -68,6 +69,10 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
|
|
||||||
return config ? (
|
return config ? (
|
||||||
<Router basename={config.basename}>
|
<Router basename={config.basename}>
|
||||||
|
<Helmet>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<title>OpenBikeSensor Portal</title>
|
||||||
|
</Helmet>
|
||||||
{config?.banner && <Banner {...config.banner} />}
|
{config?.banner && <Banner {...config.banner} />}
|
||||||
<Menu className={styles.menu}>
|
<Menu className={styles.menu}>
|
||||||
<Container>
|
<Container>
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import classnames from 'classnames'
|
import classnames from "classnames";
|
||||||
import {Container} from 'semantic-ui-react'
|
import { Container } from "semantic-ui-react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
import styles from './Page.module.less'
|
import styles from "./Page.module.less";
|
||||||
|
|
||||||
export default function Page({
|
export default function Page({
|
||||||
small,
|
small,
|
||||||
children,
|
children,
|
||||||
fullScreen,
|
fullScreen,
|
||||||
stage,
|
stage,
|
||||||
|
title,
|
||||||
}: {
|
}: {
|
||||||
small?: boolean
|
small?: boolean;
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
fullScreen?: boolean
|
fullScreen?: boolean;
|
||||||
stage?: ReactNode
|
stage?: ReactNode;
|
||||||
|
title?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{title && (
|
||||||
|
<Helmet>
|
||||||
|
<title>{title} - OpenBikeSensor Portal</title>
|
||||||
|
</Helmet>
|
||||||
|
)}
|
||||||
<main
|
<main
|
||||||
className={classnames(
|
className={classnames(
|
||||||
styles.page,
|
styles.page,
|
||||||
|
@ -27,5 +36,6 @@ export default function Page({
|
||||||
{stage}
|
{stage}
|
||||||
{fullScreen ? children : <Container>{children}</Container>}
|
{fullScreen ? children : <Container>{children}</Container>}
|
||||||
</main>
|
</main>
|
||||||
)
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ export default function ExportPage() {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const exportUrl = `${config?.apiUrl}/export/events?bbox=${bbox}&fmt=${fmt}`;
|
const exportUrl = `${config?.apiUrl}/export/events?bbox=${bbox}&fmt=${fmt}`;
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page title="Export">
|
||||||
<Header as="h2">Export</Header>
|
<Header as="h2">Export</Header>
|
||||||
|
|
||||||
<Message icon info>
|
<Message icon info>
|
||||||
|
|
|
@ -35,7 +35,7 @@ const LoginRedirectPage = connect((state) => ({loggedIn: Boolean(state.login)}))
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Page small>
|
<Page small title="Login error">
|
||||||
<Message icon error>
|
<Message icon error>
|
||||||
<Icon name="warning sign" />
|
<Icon name="warning sign" />
|
||||||
<Message.Content>
|
<Message.Content>
|
||||||
|
@ -91,7 +91,7 @@ function ExchangeAuthCode({code}) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Page small>{content}</Page>
|
return <Page small title="Login">{content}</Page>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoginRedirectPage
|
export default LoginRedirectPage
|
||||||
|
|
|
@ -148,7 +148,7 @@ export default function MapPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page fullScreen>
|
<Page fullScreen title="Map">
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
{layerSidebar && (
|
{layerSidebar && (
|
||||||
<div className={styles.mapSidebar}>
|
<div className={styles.mapSidebar}>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {Page} from '../components'
|
||||||
export default function NotFoundPage() {
|
export default function NotFoundPage() {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page title="Page not found">
|
||||||
<Header as="h2">Page not found</Header>
|
<Header as="h2">Page not found</Header>
|
||||||
<p>You know what that means...</p>
|
<p>You know what that means...</p>
|
||||||
<Button onClick={history.goBack.bind(history)}>Go back</Button>
|
<Button onClick={history.goBack.bind(history)}>Go back</Button>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(func
|
||||||
}, [setLoading, setLogin, setErrors])
|
}, [setLoading, setLogin, setErrors])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page title="Settings">
|
||||||
<Grid centered relaxed divided stackable>
|
<Grid centered relaxed divided stackable>
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
<Grid.Column width={8}>
|
<Grid.Column width={8}>
|
||||||
|
|
|
@ -106,12 +106,13 @@ const TrackEditor = connect((state) => ({login: state.login}))(function TrackEdi
|
||||||
}
|
}
|
||||||
}, [setBusy, setConfirmDelete, slug, history])
|
}, [setBusy, setConfirmDelete, slug, history])
|
||||||
|
|
||||||
|
const title = `Edit ${track ? track.title || 'Unnamed track' : 'track'}`
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page title={title}>
|
||||||
<Grid centered relaxed divided stackable>
|
<Grid centered relaxed divided stackable>
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
<Grid.Column width={10}>
|
<Grid.Column width={10}>
|
||||||
<Header as="h2">Edit {track ? track.title || 'Unnamed track' : 'track'}</Header>
|
<Header as="h2">{title}</Header>
|
||||||
<Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
|
<Form loading={loading} key={track?.slug} onSubmit={onSubmit}>
|
||||||
<Ref innerRef={findInput(register)}>
|
<Ref innerRef={findInput(register)}>
|
||||||
<Form.Input label="Title" name="title" defaultValue={track?.title} style={{fontSize: '120%'}} />
|
<Form.Input label="Title" name="title" defaultValue={track?.title} style={{fontSize: '120%'}} />
|
||||||
|
|
|
@ -176,8 +176,10 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
const [pointsMode, setPointsMode] = React.useState('overtakingEvents') // none|overtakingEvents|measurements
|
const [pointsMode, setPointsMode] = React.useState('overtakingEvents') // none|overtakingEvents|measurements
|
||||||
const [side, setSide] = React.useState('overtaker') // overtaker|stationary
|
const [side, setSide] = React.useState('overtaker') // overtaker|stationary
|
||||||
|
|
||||||
|
const title = track ? track.title || 'Unnamed track' : null
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
title={title}
|
||||||
stage={
|
stage={
|
||||||
<div className={styles.stage}>
|
<div className={styles.stage}>
|
||||||
<Loader active={loading} />
|
<Loader active={loading} />
|
||||||
|
@ -204,7 +206,7 @@ const TrackPage = connect((state) => ({login: state.login}))(function TrackPage(
|
||||||
<Segment>
|
<Segment>
|
||||||
{track && (
|
{track && (
|
||||||
<>
|
<>
|
||||||
<Header as="h1">{track.title || 'Unnamed track'}</Header>
|
<Header as="h1">{title}</Header>
|
||||||
<TrackDetails {...{track, isAuthor}} />
|
<TrackDetails {...{track, isAuthor}} />
|
||||||
<TrackActions {...{isAuthor, onDownload, slug}} />
|
<TrackActions {...{isAuthor, onDownload, slug}} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -136,9 +136,10 @@ function UploadButton({navigate, ...props}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TracksPage = connect((state) => ({login: (state as any).login}))(function TracksPage({login, privateTracks}) {
|
const TracksPage = connect((state) => ({login: (state as any).login}))(function TracksPage({login, privateTracks}) {
|
||||||
|
const title = privateTracks ? 'My tracks' : 'Public tracks'
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page title={title}>
|
||||||
<Header as='h2'>{privateTracks ? 'My tracks' : 'Public tracks'}</Header>
|
<Header as='h2'>{title}</Header>
|
||||||
{privateTracks && <Link component={UploadButton} to="/upload" />}
|
{privateTracks && <Link component={UploadButton} to="/upload" />}
|
||||||
<TrackList {...{privateTracks}} />
|
<TrackList {...{privateTracks}} />
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default function UploadPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page title="Upload">
|
||||||
{files.length ? (
|
{files.length ? (
|
||||||
<Table>
|
<Table>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
|
|
Loading…
Reference in a new issue