Add page titles using react-helmet (#1831)

* add titles with react-helmet
This commit is contained in:
stashcoder42
2021-10-26 18:37:18 -04:00
committed by GitHub
parent e9c7b0aed3
commit bdb8dc94d3
21 changed files with 349 additions and 140 deletions

View File

@@ -58,6 +58,7 @@
"react": "17.0.1",
"react-bootstrap": "1.4.3",
"react-dom": "17.0.1",
"react-helmet": "^6.1.0",
"react-intl": "^5.10.16",
"react-jw-player": "1.19.1",
"react-markdown": "^5.0.3",
@@ -89,6 +90,7 @@
"@types/node": "14.14.22",
"@types/react": "17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.3",
"@types/react-router-bootstrap": "^0.24.5",
"@types/react-router-dom": "5.1.7",
"@types/react-router-hash-link": "^1.2.1",

View File

@@ -1,6 +1,7 @@
import React, { useEffect } from "react";
import { Route, Switch, useRouteMatch } from "react-router-dom";
import { IntlProvider } from "react-intl";
import { Helmet } from "react-helmet";
import { mergeWith } from "lodash";
import { ToastProvider } from "src/hooks/Toast";
import LightboxProvider from "src/hooks/Lightbox/context";
@@ -30,7 +31,7 @@ import Images from "./components/Images/Images";
import { Setup } from "./components/Setup/Setup";
import { Migrate } from "./components/Setup/Migrate";
import * as GQL from "./core/generated-graphql";
import { LoadingIndicator } from "./components/Shared";
import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared";
import ConfigurationProvider from "./hooks/Config";
initPolyfills();
@@ -145,6 +146,10 @@ export const App: React.FC = () => {
>
<ToastProvider>
<LightboxProvider>
<Helmet
titleTemplate={`%s ${TITLE_SUFFIX}`}
defaultTitle="Stash"
/>
{maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div>
</LightboxProvider>

View File

@@ -5,6 +5,7 @@
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))
### 🎨 Improvements
* Added specific page titles. ([#1831](https://github.com/stashapp/stash/pull/1831))
* Added es-ES language option. ([#1886](https://github.com/stashapp/stash/pull/1886))
* Show pagination at top and bottom of page. ([#1776](https://github.com/stashapp/stash/pull/1776))
* Include total duration/megapixels and filesize information on Scenes and Images pages. ([#1776](https://github.com/stashapp/stash/pull/1776))

View File

@@ -1,11 +1,25 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook";
import Gallery from "./GalleryDetails/Gallery";
import GalleryCreate from "./GalleryDetails/GalleryCreate";
import { GalleryList } from "./GalleryList";
const Galleries = () => (
const Galleries = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "galleries",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route
exact
@@ -17,6 +31,8 @@ const Galleries = () => (
<Route exact path="/galleries/new" component={GalleryCreate} />
<Route path="/galleries/:id/:tab?" component={Gallery} />
</Switch>
</>
);
};
export default Galleries;

View File

@@ -2,6 +2,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { useParams, useHistory, Link } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import * as GQL from "src/core/generated-graphql";
import {
mutateMetadataScan,
@@ -270,6 +271,11 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
return (
<div className="row">
<Helmet>
<title>
{gallery.title ?? TextUtils.fileNameFromPath(gallery.path ?? "")}
</title>
</Helmet>
{maybeRenderDeleteDialog()}
<div className="gallery-tabs">
<div className="d-none d-xl-block">

View File

@@ -2,6 +2,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useHistory, Link } from "react-router-dom";
import { Helmet } from "react-helmet";
import {
useFindImage,
useImageIncrementO,
@@ -262,6 +263,10 @@ export const Image: React.FC = () => {
return (
<div className="row">
<Helmet>
<title>{image.title ?? TextUtils.fileNameFromPath(image.path)}</title>
</Helmet>
{maybeRenderDeleteDialog()}
<div className="image-tabs order-xl-first order-last">
<div className="d-none d-xl-block">

View File

@@ -1,10 +1,24 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook";
import { Image } from "./ImageDetails/Image";
import { ImageList } from "./ImageList";
const Images = () => (
const Images: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "images",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route
exact
@@ -15,6 +29,8 @@ const Images = () => (
/>
<Route path="/images/:id" component={Image} />
</Switch>
</>
);
};
export default Images;

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import {
@@ -174,6 +175,10 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
// TODO: CSS class
return (
<div className="row">
<Helmet>
<title>{movie?.name}</title>
</Helmet>
<div className="movie-details mb-3 col col-xl-4 col-lg-6">
<div className="logo w-100">
{encodingImage ? (

View File

@@ -1,15 +1,31 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Movie from "./MovieDetails/Movie";
import MovieCreate from "./MovieDetails/MovieCreate";
import { MovieList } from "./MovieList";
const Movies = () => (
const Movies: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "movies",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route exact path="/movies" component={MovieList} />
<Route exact path="/movies/new" component={MovieCreate} />
<Route path="/movies/:id/:tab?" component={Movie} />
</Switch>
</>
);
};
export default Movies;

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { Button, Tabs, Tab } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useHistory } from "react-router-dom";
import { Helmet } from "react-helmet";
import cx from "classnames";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
@@ -299,6 +300,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
return (
<div id="performer-page" className="row">
<Helmet>
<title>{performer.name}</title>
</Helmet>
<div className="performer-image-container col-md-4 text-center">
{imageEncoding ? (
<LoadingIndicator message="Encoding image..." />

View File

@@ -1,11 +1,25 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook";
import Performer from "./PerformerDetails/Performer";
import PerformerCreate from "./PerformerDetails/PerformerCreate";
import { PerformerList } from "./PerformerList";
const Performers = () => (
const Performers: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "performers",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route
exact
@@ -17,6 +31,7 @@ const Performers = () => (
<Route path="/performers/new" component={PerformerCreate} />
<Route path="/performers/:id/:tab?" component={Performer} />
</Switch>
</>
);
};
export default Performers;

View File

@@ -3,6 +3,7 @@ import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
import { Helmet } from "react-helmet";
import * as GQL from "src/core/generated-graphql";
import {
mutateMetadataScan,
@@ -566,6 +567,9 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
return (
<div className="row">
<Helmet>
<title>{scene.title ?? TextUtils.fileNameFromPath(scene.path)}</title>
</Helmet>
{maybeRenderSceneGenerateDialog()}
{maybeRenderDeleteDialog()}
<div

View File

@@ -2,6 +2,8 @@ import _ from "lodash";
import React from "react";
import { useHistory } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Mousetrap from "mousetrap";
import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
import { queryFindSceneMarkers } from "src/core/StashService";
@@ -81,6 +83,18 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
);
}
}
const title_template = `${intl.formatMessage({
id: "markers",
})} ${TITLE_SUFFIX}`;
return listData.template;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
{listData.template}
</>
);
};

View File

@@ -1,11 +1,25 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { PersistanceLevel } from "src/hooks/ListHook";
import Scene from "./SceneDetails/Scene";
import { SceneList } from "./SceneList";
import { SceneMarkerList } from "./SceneMarkerList";
const Scenes = () => (
const Scenes: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "scenes",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route
exact
@@ -17,6 +31,7 @@ const Scenes = () => (
<Route exact path="/scenes/markers" component={SceneMarkerList} />
<Route path="/scenes/:id" component={Scene} />
</Switch>
</>
);
};
export default Scenes;

View File

@@ -2,7 +2,9 @@ import React from "react";
import queryString from "query-string";
import { Card, Tab, Nav, Row, Col } from "react-bootstrap";
import { useHistory, useLocation } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import { SettingsAboutPanel } from "./SettingsAboutPanel";
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel";
@@ -14,14 +16,22 @@ import { SettingsToolsPanel } from "./SettingsToolsPanel";
import { SettingsDLNAPanel } from "./SettingsDLNAPanel";
export const Settings: React.FC = () => {
const intl = useIntl();
const location = useLocation();
const history = useHistory();
const defaultTab = queryString.parse(location.search).tab ?? "tasks";
const onSelect = (val: string) => history.push(`?tab=${val}`);
const title_template = `${intl.formatMessage({
id: "settings",
})} ${TITLE_SUFFIX}`;
return (
<Card className="col col-lg-9 mx-auto">
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Tab.Container
activeKey={defaultTab}
id="configuration-tabs"

View File

@@ -18,3 +18,4 @@ export { RatingStars } from "./RatingStars";
export { ExportDialog } from "./ExportDialog";
export { default as DeleteEntityDialog } from "./DeleteEntityDialog";
export { OperationButton } from "./OperationButton";
export const TITLE_SUFFIX = " | Stash";

View File

@@ -2,6 +2,7 @@ import { Tabs, Tab } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
@@ -179,6 +180,11 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
</div>
{!isEditing ? (
<>
<Helmet>
<title>
{studio.name ?? intl.formatMessage({ id: "studio" })}
</title>
</Helmet>
<StudioDetailsPanel studio={studio} />
<DetailsEditNavbar
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}

View File

@@ -1,15 +1,30 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Studio from "./StudioDetails/Studio";
import StudioCreate from "./StudioDetails/StudioCreate";
import { StudioList } from "./StudioList";
const Studios = () => (
const Studios: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "studios",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route exact path="/studios" component={StudioList} />
<Route exact path="/studios/new" component={StudioCreate} />
<Route path="/studios/:id/:tab?" component={Studio} />
</Switch>
</>
);
};
export default Studios;

View File

@@ -2,6 +2,7 @@ import { Tabs, Tab, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
@@ -248,6 +249,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
}
return (
<>
<Helmet>
<title>{tag.name}</title>
</Helmet>
<div className="row">
<div className="tag-details col-md-4">
<div className="text-center logo-container">
@@ -304,7 +309,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
>
<TagGalleriesPanel tag={tag} />
</Tab>
<Tab eventKey="markers" title={intl.formatMessage({ id: "markers" })}>
<Tab
eventKey="markers"
title={intl.formatMessage({ id: "markers" })}
>
<TagMarkersPanel tag={tag} />
</Tab>
<Tab
@@ -318,6 +326,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
{renderDeleteAlert()}
{renderMergeDialog()}
</div>
</>
);
};

View File

@@ -1,15 +1,31 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useIntl } from "react-intl";
import { Helmet } from "react-helmet";
import { TITLE_SUFFIX } from "src/components/Shared";
import Tag from "./TagDetails/Tag";
import TagCreate from "./TagDetails/TagCreate";
import { TagList } from "./TagList";
const Tags = () => (
const Tags: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({
id: "tags",
})} ${TITLE_SUFFIX}`;
return (
<>
<Helmet
defaultTitle={title_template}
titleTemplate={`%s | ${title_template}`}
/>
<Switch>
<Route exact path="/tags" component={TagList} />
<Route exact path="/tags/new" component={TagCreate} />
<Route path="/tags/:id/:tab?" component={Tag} />
</Switch>
</>
);
};
export default Tags;

View File

@@ -3072,6 +3072,13 @@
dependencies:
"@types/react" "*"
"@types/react-helmet@^6.1.3":
version "6.1.3"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.3.tgz#1a58b26a79e464c59d3f9cdd5b7ece485335937b"
integrity sha512-U4onVxaZxAp78KpXsfmyCIhLjsvJJ3goG3CYFOo+xW0cPYAz9oe5cBAUSAcN7l35OTbrFvu9TuE0YkcZMKGr4A==
dependencies:
"@types/react" "*"
"@types/react-router-bootstrap@^0.24.5":
version "0.24.5"
resolved "https://registry.yarnpkg.com/@types/react-router-bootstrap/-/react-router-bootstrap-0.24.5.tgz#9257ba3dfb01cda201aac9fa05cde3eb09ea5b27"
@@ -12635,6 +12642,21 @@ react-fast-compare@^2.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fast-compare@^3.1.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
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"
react-input-autosize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85"
@@ -12836,6 +12858,11 @@ react-select@^4.0.2:
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0"
react-side-effect@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3"
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
react-transition-group@^4.3.0, react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"