Add dynamic titles to all pages via react-helmet (related to #148)

This commit is contained in:
Paul Bienkowski 2022-03-04 13:07:56 +01:00
parent 40d23c537e
commit 8f2861a8c9
13 changed files with 91 additions and 31 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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>

View file

@ -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>
) </>
);
} }

View file

@ -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>

View file

@ -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

View file

@ -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}>

View file

@ -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>

View file

@ -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}>

View file

@ -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%'}} />

View file

@ -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}} />
</> </>

View file

@ -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>

View file

@ -135,7 +135,7 @@ export default function UploadPage() {
} }
return ( return (
<Page> <Page title="Upload">
{files.length ? ( {files.length ? (
<Table> <Table>
<Table.Header> <Table.Header>