Link improvements and fixes (#4501)

* Add ExternalLink
* Replace <a> with <Link>
This commit is contained in:
DingDongSoLong4
2024-02-06 01:21:19 +02:00
committed by GitHub
parent 1d0fa27c71
commit cf8efa9035
28 changed files with 292 additions and 325 deletions

View File

@@ -9,6 +9,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
import { getStashboxBase } from "src/utils/stashbox"; import { getStashboxBase } from "src/utils/stashbox";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "../Shared/ExternalLink";
interface IProps { interface IProps {
type: "scene" | "performer"; type: "scene" | "performer";
@@ -108,12 +109,12 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
<FormattedMessage id="stashbox.submission_successful" /> <FormattedMessage id="stashbox.submission_successful" />
</h6> </h6>
<div> <div>
<a target="_blank" rel="noreferrer noopener" href={reviewUrl}> <ExternalLink href={reviewUrl}>
<FormattedMessage <FormattedMessage
id="stashbox.go_review_draft" id="stashbox.go_review_draft"
values={{ endpoint_name: selectedBox?.name }} values={{ endpoint_name: selectedBox?.name }}
/> />
</a> </ExternalLink>
</div> </div>
</> </>
); );

View File

@@ -38,6 +38,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
import { useRatingKeybinds } from "src/hooks/keybinds"; import { useRatingKeybinds } from "src/hooks/keybinds";
import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IProps { interface IProps {
movie: GQL.MovieDataFragment; movie: GQL.MovieDataFragment;
@@ -274,15 +275,13 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
const renderClickableIcons = () => ( const renderClickableIcons = () => (
<span className="name-icons"> <span className="name-icons">
{movie.url && ( {movie.url && (
<Button className="minimal icon-link" title={movie.url}> <Button
<a as={ExternalLink}
href={TextUtils.sanitiseURL(movie.url)} href={TextUtils.sanitiseURL(movie.url)}
className="link" className="minimal link"
target="_blank" title={movie.url}
rel="noopener noreferrer" >
> <Icon icon={faLink} />
<Icon icon={faLink} />
</a>
</Button> </Button>
)} )}
</span> </span>

View File

@@ -3,6 +3,7 @@ import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { DetailItem } from "src/components/Shared/DetailItem"; import { DetailItem } from "src/components/Shared/DetailItem";
import { Link } from "react-router-dom";
interface IMovieDetailsPanel { interface IMovieDetailsPanel {
movie: GQL.MovieDataFragment; movie: GQL.MovieDataFragment;
@@ -34,9 +35,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
id="studio" id="studio"
value={ value={
movie.studio?.id ? ( movie.studio?.id ? (
<a href={`/studios/${movie.studio?.id}`} target="_self"> <Link to={`/studios/${movie.studio?.id}`}>
{movie.studio?.name} {movie.studio?.name}
</a> </Link>
) : ( ) : (
"" ""
) )

View File

@@ -45,6 +45,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
import { DetailImage } from "src/components/Shared/DetailImage"; import { DetailImage } from "src/components/Shared/DetailImage";
import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IProps { interface IProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
@@ -493,57 +494,50 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
<Icon icon={faHeart} /> <Icon icon={faHeart} />
</Button> </Button>
{performer.url && ( {performer.url && (
<Button className="minimal icon-link" title={performer.url}> <Button
<a as={ExternalLink}
href={TextUtils.sanitiseURL(performer.url)} href={TextUtils.sanitiseURL(performer.url)}
className="link" className="minimal link"
target="_blank" title={performer.url}
rel="noopener noreferrer" >
> <Icon icon={faLink} />
<Icon icon={faLink} />
</a>
</Button> </Button>
)} )}
{(urls ?? []).map((url, index) => ( {(urls ?? []).map((url, index) => (
<Button key={index} className="minimal icon-link" title={url}> <Button
<a key={index}
href={TextUtils.sanitiseURL(url)} as={ExternalLink}
className={`detail-link ${index}`} href={TextUtils.sanitiseURL(url)}
target="_blank" className={`minimal link detail-link detail-link-${index}`}
rel="noopener noreferrer" title={url}
> >
<Icon icon={faLink} /> <Icon icon={faLink} />
</a>
</Button> </Button>
))} ))}
{performer.twitter && ( {performer.twitter && (
<Button className="minimal icon-link" title={performer.twitter}> <Button
<a as={ExternalLink}
href={TextUtils.sanitiseURL( href={TextUtils.sanitiseURL(
performer.twitter, performer.twitter,
TextUtils.twitterURL TextUtils.twitterURL
)} )}
className="twitter" className="minimal link twitter"
target="_blank" title={performer.twitter}
rel="noopener noreferrer" >
> <Icon icon={faTwitter} />
<Icon icon={faTwitter} />
</a>
</Button> </Button>
)} )}
{performer.instagram && ( {performer.instagram && (
<Button className="minimal icon-link" title={performer.instagram}> <Button
<a as={ExternalLink}
href={TextUtils.sanitiseURL( href={TextUtils.sanitiseURL(
performer.instagram, performer.instagram,
TextUtils.instagramURL TextUtils.instagramURL
)} )}
className="instagram" className="minimal link instagram"
target="_blank" title={performer.instagram}
rel="noopener noreferrer" >
> <Icon icon={faInstagram} />
<Icon icon={faInstagram} />
</a>
</Button> </Button>
)} )}
</span> </span>

View File

@@ -25,6 +25,7 @@ import {
Option as SelectOption, Option as SelectOption,
} from "../Shared/FilterSelect"; } from "../Shared/FilterSelect";
import { useCompare } from "src/hooks/state"; import { useCompare } from "src/hooks/state";
import { Link } from "react-router-dom";
export type SelectObject = { export type SelectObject = {
id: string; id: string;
@@ -86,10 +87,9 @@ export const PerformerSelect: React.FC<
...optionProps, ...optionProps,
children: ( children: (
<span className="react-select-image-option"> <span className="react-select-image-option">
<a <Link
href={`/performers/${object.id}`} to={`/performers/${object.id}`}
target="_blank" target="_blank"
rel="noreferrer"
className="performer-select-image-link" className="performer-select-image-link"
> >
<img <img
@@ -97,7 +97,7 @@ export const PerformerSelect: React.FC<
src={object.image_path ?? ""} src={object.image_path ?? ""}
loading="lazy" loading="lazy"
/> />
</a> </Link>
<span>{name}</span> <span>{name}</span>
{object.disambiguation && ( {object.disambiguation && (
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span> <span className="performer-disambiguation">{` (${object.disambiguation})`}</span>

View File

@@ -27,22 +27,9 @@
color: #ff7373; color: #ff7373;
} }
.link {
color: rgb(191, 204, 214);
}
.instagram { .instagram {
color: pink; color: pink;
} }
.icon-link {
padding: 0;
a {
display: inline-block;
padding: $btn-padding-y $btn-padding-x;
}
}
} }
.rating-number .form-control { .rating-number .form-control {

View File

@@ -85,7 +85,7 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
url={NavUtils.makeScenesPHashMatchUrl(phash?.value)} url={NavUtils.makeScenesPHashMatchUrl(phash?.value)}
target="_self" target="_self"
truncate truncate
trusted internal
/> />
<URLField <URLField
id="path" id="path"

View File

@@ -2,6 +2,7 @@ import React from "react";
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useLatestVersion } from "src/core/StashService"; import { useLatestVersion } from "src/core/StashService";
import { ExternalLink } from "../Shared/ExternalLink";
import { ConstantSetting, SettingGroup } from "./Inputs"; import { ConstantSetting, SettingGroup } from "./Inputs";
import { SettingSection } from "./SettingSection"; import { SettingSection } from "./SettingSection";
@@ -115,13 +116,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_home" }, { id: "config.about.stash_home" },
{ {
url: ( url: (
<a <ExternalLink href="https://github.com/stashapp/stash">
href="https://github.com/stashapp/stash"
rel="noopener noreferrer"
target="_blank"
>
GitHub GitHub
</a> </ExternalLink>
), ),
} }
)} )}
@@ -131,13 +128,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_wiki" }, { id: "config.about.stash_wiki" },
{ {
url: ( url: (
<a <ExternalLink href="https://docs.stashapp.cc">
href="https://docs.stashapp.cc"
rel="noopener noreferrer"
target="_blank"
>
Documentation Documentation
</a> </ExternalLink>
), ),
} }
)} )}
@@ -147,13 +140,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_discord" }, { id: "config.about.stash_discord" },
{ {
url: ( url: (
<a <ExternalLink href="https://discord.gg/2TsNFKt">
href="https://discord.gg/2TsNFKt"
rel="noopener noreferrer"
target="_blank"
>
Discord Discord
</a> </ExternalLink>
), ),
} }
)} )}
@@ -163,13 +152,9 @@ export const SettingsAboutPanel: React.FC = () => {
{ id: "config.about.stash_open_collective" }, { id: "config.about.stash_open_collective" },
{ {
url: ( url: (
<a <ExternalLink href="https://opencollective.com/stashapp">
href="https://opencollective.com/stashapp"
rel="noopener noreferrer"
target="_blank"
>
Open Collective Open Collective
</a> </ExternalLink>
), ),
} }
)} )}

View File

@@ -7,6 +7,7 @@ import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs";
import { useSettings } from "./context"; import { useSettings } from "./context";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "../Shared/ExternalLink";
export const SettingsLibraryPanel: React.FC = () => { export const SettingsLibraryPanel: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
@@ -76,13 +77,9 @@ export const SettingsLibraryPanel: React.FC = () => {
{intl.formatMessage({ {intl.formatMessage({
id: "config.general.excluded_video_patterns_desc", id: "config.general.excluded_video_patterns_desc",
})} })}
<a <ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
rel="noopener noreferrer"
target="_blank"
>
<Icon icon={faQuestionCircle} /> <Icon icon={faQuestionCircle} />
</a> </ExternalLink>
</span> </span>
} }
value={general.excludes ?? undefined} value={general.excludes ?? undefined}
@@ -98,13 +95,9 @@ export const SettingsLibraryPanel: React.FC = () => {
{intl.formatMessage({ {intl.formatMessage({
id: "config.general.excluded_image_gallery_patterns_desc", id: "config.general.excluded_image_gallery_patterns_desc",
})} })}
<a <ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
rel="noopener noreferrer"
target="_blank"
>
<Icon icon={faQuestionCircle} /> <Icon icon={faQuestionCircle} />
</a> </ExternalLink>
</span> </span>
} }
value={general.imageExcludes ?? undefined} value={general.imageExcludes ?? undefined}

View File

@@ -26,6 +26,7 @@ import {
AvailablePluginPackages, AvailablePluginPackages,
InstalledPluginPackages, InstalledPluginPackages,
} from "./PluginPackageManager"; } from "./PluginPackageManager";
import { ExternalLink } from "../Shared/ExternalLink";
interface IPluginSettingProps { interface IPluginSettingProps {
pluginID: string; pluginID: string;
@@ -97,15 +98,12 @@ export const SettingsPluginsPanel: React.FC = () => {
function renderLink(url?: string) { function renderLink(url?: string) {
if (url) { if (url) {
return ( return (
<Button className="minimal"> <Button
<a as={ExternalLink}
href={TextUtils.sanitiseURL(url)} href={TextUtils.sanitiseURL(url)}
className="link" className="minimal link"
target="_blank" >
rel="noopener noreferrer" <Icon icon={faLink} />
>
<Icon icon={faLink} />
</a>
</Button> </Button>
); );
} }

View File

@@ -23,6 +23,7 @@ import {
AvailableScraperPackages, AvailableScraperPackages,
InstalledScraperPackages, InstalledScraperPackages,
} from "./ScraperPackageManager"; } from "./ScraperPackageManager";
import { ExternalLink } from "../Shared/ExternalLink";
interface IURLList { interface IURLList {
urls: string[]; urls: string[];
@@ -42,16 +43,7 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
const sanitised = TextUtils.sanitiseURL(url); const sanitised = TextUtils.sanitiseURL(url);
const siteURL = linkSite(sanitised!); const siteURL = linkSite(sanitised!);
return ( return <ExternalLink href={siteURL}>{sanitised}</ExternalLink>;
<a
href={siteURL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
{sanitised}
</a>
);
} }
} }

View File

@@ -67,7 +67,7 @@
min-width: 100px; min-width: 100px;
text-align: right; text-align: right;
button { .btn {
margin: 0.25rem; margin: 0.25rem;
} }
} }

View File

@@ -5,6 +5,7 @@ import { useHistory } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useSystemStatus, mutateMigrate } from "src/core/StashService"; import { useSystemStatus, mutateMigrate } from "src/core/StashService";
import { migrationNotes } from "src/docs/en/MigrationNotes"; import { migrationNotes } from "src/docs/en/MigrationNotes";
import { ExternalLink } from "../Shared/ExternalLink";
import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { MarkdownPage } from "../Shared/MarkdownPage"; import { MarkdownPage } from "../Shared/MarkdownPage";
@@ -35,18 +36,12 @@ export const Migrate: React.FC = () => {
: ""; : "";
const discordLink = ( const discordLink = (
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer"> <ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
Discord
</a>
); );
const githubLink = ( const githubLink = (
<a <ExternalLink href="https://github.com/stashapp/stash/issues">
href="https://github.com/stashapp/stash/issues"
target="_blank"
rel="noreferrer"
>
<FormattedMessage id="setup.github_repository" /> <FormattedMessage id="setup.github_repository" />
</a> </ExternalLink>
); );
useEffect(() => { useEffect(() => {

View File

@@ -27,6 +27,7 @@ import {
faQuestionCircle, faQuestionCircle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { releaseNotes } from "src/docs/en/ReleaseNotes"; import { releaseNotes } from "src/docs/en/ReleaseNotes";
import { ExternalLink } from "../Shared/ExternalLink";
export const Setup: React.FC = () => { export const Setup: React.FC = () => {
const { configuration, loading: configLoading } = const { configuration, loading: configLoading } =
@@ -103,18 +104,12 @@ export const Setup: React.FC = () => {
}, [configuration]); }, [configuration]);
const discordLink = ( const discordLink = (
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer"> <ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
Discord
</a>
); );
const githubLink = ( const githubLink = (
<a <ExternalLink href="https://github.com/stashapp/stash/issues">
href="https://github.com/stashapp/stash/issues"
target="_blank"
rel="noreferrer"
>
<FormattedMessage id="setup.github_repository" /> <FormattedMessage id="setup.github_repository" />
</a> </ExternalLink>
); );
function onConfigLocationChosen(inWorkDir: boolean) { function onConfigLocationChosen(inWorkDir: boolean) {
@@ -825,14 +820,9 @@ export const Setup: React.FC = () => {
id="setup.success.open_collective" id="setup.success.open_collective"
values={{ values={{
open_collective_link: ( open_collective_link: (
<a <ExternalLink href="https://opencollective.com/stashapp">
href="https://opencollective.com/stashapp" Open Collective
target="_blank" </ExternalLink>
rel="noreferrer"
>
{" "}
OpenCollective{" "}
</a>
), ),
}} }}
/> />

View File

@@ -0,0 +1,5 @@
type IExternalLinkProps = JSX.IntrinsicElements["a"];
export const ExternalLink: React.FC<IExternalLinkProps> = (props) => {
return <a target="_blank" rel="noopener noreferrer" {...props} />;
};

View File

@@ -2,6 +2,7 @@ import React, { useMemo } from "react";
import { StashId } from "src/core/generated-graphql"; import { StashId } from "src/core/generated-graphql";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import { getStashboxBase } from "src/utils/stashbox"; import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "./ExternalLink";
export type LinkType = "performers" | "scenes" | "studios"; export type LinkType = "performers" | "scenes" | "studios";
@@ -26,9 +27,7 @@ export const StashIDPill: React.FC<{
return ( return (
<span className="stash-id-pill" data-endpoint={endpointName}> <span className="stash-id-pill" data-endpoint={endpointName}>
<span>{endpointName}</span> <span>{endpointName}</span>
<a href={link} target="_blank" rel="noopener noreferrer"> <ExternalLink href={link}>{stash_id}</ExternalLink>
{stash_id}
</a>
</span> </span>
); );
}; };

View File

@@ -45,6 +45,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
import { useRatingKeybinds } from "src/hooks/keybinds"; import { useRatingKeybinds } from "src/hooks/keybinds";
import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@@ -285,15 +286,13 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
const renderClickableIcons = () => ( const renderClickableIcons = () => (
<span className="name-icons"> <span className="name-icons">
{studio.url && ( {studio.url && (
<Button className="minimal icon-link" title={studio.url}> <Button
<a as={ExternalLink}
href={TextUtils.sanitiseURL(studio.url)} href={TextUtils.sanitiseURL(studio.url)}
className="link" className="minimal link"
target="_blank" title={studio.url}
rel="noopener noreferrer" >
> <Icon icon={faLink} />
<Icon icon={faLink} />
</a>
</Button> </Button>
)} )}
</span> </span>

View File

@@ -2,6 +2,7 @@ import React from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { DetailItem } from "src/components/Shared/DetailItem"; import { DetailItem } from "src/components/Shared/DetailItem";
import { StashIDPill } from "src/components/Shared/StashID"; import { StashIDPill } from "src/components/Shared/StashID";
import { Link } from "react-router-dom";
interface IStudioDetailsPanel { interface IStudioDetailsPanel {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@@ -51,9 +52,9 @@ export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
id="parent_studios" id="parent_studios"
value={ value={
studio.parent_studio?.name ? ( studio.parent_studio?.name ? (
<a href={`/studios/${studio.parent_studio?.id}`} target="_self"> <Link to={`/studios/${studio.parent_studio?.id}`}>
{studio.parent_studio.name} {studio.parent_studio.name}
</a> </Link>
) : ( ) : (
"" ""
) )

View File

@@ -18,6 +18,7 @@ import {
faExternalLinkAlt, faExternalLinkAlt,
faTimes, faTimes,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "../Shared/ExternalLink";
interface IPerformerModalProps { interface IPerformerModalProps {
performer: GQL.ScrapedScenePerformerDataFragment; performer: GQL.ScrapedScenePerformerDataFragment;
@@ -82,12 +83,14 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
[name]: !excluded[name], [name]: !excluded[name],
}); });
const renderField = ( function maybeRenderField(
name: string, name: string,
text: string | null | undefined, text: string | null | undefined,
truncate: boolean = true truncate: boolean = true
) => ) {
text && ( if (!text) return;
return (
<div className="row no-gutters"> <div className="row no-gutters">
<div className="col-5 performer-create-modal-field" key={name}> <div className="col-5 performer-create-modal-field" key={name}>
{!create && ( {!create && (
@@ -112,11 +115,72 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
)} )}
</div> </div>
); );
}
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0]; function maybeRenderImage() {
const link = base if (!images.length) return;
? `${base}performers/${performer.remote_site_id}`
: undefined; return (
<div className="col-5 image-selection">
<div className="performer-image">
{!create && (
<Button
onClick={() => toggleField("image")}
variant="secondary"
className={cx(
"performer-image-exclude",
excluded.image ? "text-muted" : "text-success"
)}
>
<Icon icon={excluded.image ? faTimes : faCheck} />
</Button>
)}
<img
src={images[imageIndex]}
className={cx({ "d-none": imageState !== "loaded" })}
alt=""
onLoad={() => handleLoad(imageIndex)}
onError={handleError}
/>
{imageState === "loading" && (
<LoadingIndicator message="Loading image..." />
)}
{imageState === "error" && (
<div className="h-100 d-flex justify-content-center align-items-center">
<b>Error loading image.</b>
</div>
)}
</div>
<div className="d-flex mt-3">
<Button onClick={setPrev} disabled={images.length === 1}>
<Icon icon={faArrowLeft} />
</Button>
<h5 className="flex-grow-1">
Select performer image
<br />
{imageIndex + 1} of {images.length}
</h5>
<Button onClick={setNext} disabled={images.length === 1}>
<Icon icon={faArrowRight} />
</Button>
</div>
</div>
);
}
function maybeRenderStashBoxLink() {
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
if (!base) return;
return (
<h6 className="mt-2">
<ExternalLink href={`${base}performers/${performer.remote_site_id}`}>
<FormattedMessage id="stashbox.source" />
<Icon icon={faExternalLinkAlt} className="ml-2" />
</ExternalLink>
</h6>
);
}
function onSaveClicked() { function onSaveClicked() {
if (!performer.name) { if (!performer.name) {
@@ -201,89 +265,37 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
> >
<div className="row"> <div className="row">
<div className="col-7"> <div className="col-7">
{renderField("name", performer.name)} {maybeRenderField("name", performer.name)}
{renderField("disambiguation", performer.disambiguation)} {maybeRenderField("disambiguation", performer.disambiguation)}
{renderField("aliases", performer.aliases)} {maybeRenderField("aliases", performer.aliases)}
{renderField( {maybeRenderField(
"gender", "gender",
performer.gender performer.gender
? intl.formatMessage({ id: "gender_types." + performer.gender }) ? intl.formatMessage({ id: "gender_types." + performer.gender })
: "" : ""
)} )}
{renderField("birthdate", performer.birthdate)} {maybeRenderField("birthdate", performer.birthdate)}
{renderField("death_date", performer.death_date)} {maybeRenderField("death_date", performer.death_date)}
{renderField("ethnicity", performer.ethnicity)} {maybeRenderField("ethnicity", performer.ethnicity)}
{renderField("country", getCountryByISO(performer.country))} {maybeRenderField("country", getCountryByISO(performer.country))}
{renderField("hair_color", performer.hair_color)} {maybeRenderField("hair_color", performer.hair_color)}
{renderField("eye_color", performer.eye_color)} {maybeRenderField("eye_color", performer.eye_color)}
{renderField("height", performer.height)} {maybeRenderField("height", performer.height)}
{renderField("weight", performer.weight)} {maybeRenderField("weight", performer.weight)}
{renderField("measurements", performer.measurements)} {maybeRenderField("measurements", performer.measurements)}
{performer?.gender !== GQL.GenderEnum.Male && {performer?.gender !== GQL.GenderEnum.Male &&
renderField("fake_tits", performer.fake_tits)} maybeRenderField("fake_tits", performer.fake_tits)}
{renderField("career_length", performer.career_length)} {maybeRenderField("career_length", performer.career_length)}
{renderField("tattoos", performer.tattoos, false)} {maybeRenderField("tattoos", performer.tattoos, false)}
{renderField("piercings", performer.piercings, false)} {maybeRenderField("piercings", performer.piercings, false)}
{renderField("weight", performer.weight, false)} {maybeRenderField("weight", performer.weight, false)}
{renderField("details", performer.details)} {maybeRenderField("details", performer.details)}
{renderField("url", performer.url)} {maybeRenderField("url", performer.url)}
{renderField("twitter", performer.twitter)} {maybeRenderField("twitter", performer.twitter)}
{renderField("instagram", performer.instagram)} {maybeRenderField("instagram", performer.instagram)}
{link && ( {maybeRenderStashBoxLink()}
<h6 className="mt-2">
<a href={link} target="_blank" rel="noopener noreferrer">
<FormattedMessage id="stashbox.source" />
<Icon icon={faExternalLinkAlt} className="ml-2" />
</a>
</h6>
)}
</div> </div>
{images.length > 0 && ( {maybeRenderImage()}
<div className="col-5 image-selection">
<div className="performer-image">
{!create && (
<Button
onClick={() => toggleField("image")}
variant="secondary"
className={cx(
"performer-image-exclude",
excluded.image ? "text-muted" : "text-success"
)}
>
<Icon icon={excluded.image ? faTimes : faCheck} />
</Button>
)}
<img
src={images[imageIndex]}
className={cx({ "d-none": imageState !== "loaded" })}
alt=""
onLoad={() => handleLoad(imageIndex)}
onError={handleError}
/>
{imageState === "loading" && (
<LoadingIndicator message="Loading image..." />
)}
{imageState === "error" && (
<div className="h-100 d-flex justify-content-center align-items-center">
<b>Error loading image.</b>
</div>
)}
</div>
<div className="d-flex mt-3">
<Button onClick={setPrev} disabled={images.length === 1}>
<Icon icon={faArrowLeft} />
</Button>
<h5 className="flex-grow-1">
Select performer image
<br />
{imageIndex + 1} of {images.length}
</h5>
<Button onClick={setNext} disabled={images.length === 1}>
<Icon icon={faArrowRight} />
</Button>
</div>
</div>
)}
</div> </div>
</ModalComponent> </ModalComponent>
); );

View File

@@ -26,6 +26,7 @@ import PerformerModal from "../PerformerModal";
import { useUpdatePerformer } from "../queries"; import { useUpdatePerformer } from "../queries";
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
import { mergeStashIDs } from "src/utils/stashbox"; import { mergeStashIDs } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
type JobFragment = Pick< type JobFragment = Pick<
GQL.Job, GQL.Job,
@@ -466,14 +467,12 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
if (stashID !== undefined) { if (stashID !== undefined) {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? ( const link = base ? (
<a <ExternalLink
className="small d-block" className="small d-block"
href={`${base}performers/${stashID.stash_id}`} href={`${base}performers/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
> >
{stashID.stash_id} {stashID.stash_id}
</a> </ExternalLink>
) : ( ) : (
<div className="small">{stashID.stash_id}</div> <div className="small">{stashID.stash_id}</div>
); );

View File

@@ -12,6 +12,7 @@ import {
PerformerSelect, PerformerSelect,
} from "src/components/Performers/PerformerSelect"; } from "src/components/Performers/PerformerSelect";
import { getStashboxBase } from "src/utils/stashbox"; import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IPerformerName { interface IPerformerName {
performer: GQL.ScrapedPerformer | Performer; performer: GQL.ScrapedPerformer | Performer;
@@ -26,9 +27,7 @@ const PerformerName: React.FC<IPerformerName> = ({
}) => { }) => {
const name = const name =
baseURL && id ? ( baseURL && id ? (
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer"> <ExternalLink href={`${baseURL}${id}`}>{performer.name}</ExternalLink>
{performer.name}
</a>
) : ( ) : (
performer.name performer.name
); );

View File

@@ -24,6 +24,7 @@ import StudioResult from "./StudioResult";
import { useInitialState } from "src/hooks/state"; import { useInitialState } from "src/hooks/state";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { getStashboxBase } from "src/utils/stashbox"; import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
const getDurationStatus = ( const getDurationStatus = (
scene: IScrapedScene, scene: IScrapedScene,
@@ -488,14 +489,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
const url = scene.urls?.length ? scene.urls[0] : null; const url = scene.urls?.length ? scene.urls[0] : null;
const sceneTitleEl = url ? ( const sceneTitleEl = url ? (
<a <ExternalLink className="scene-link" href={url}>
href={url}
target="_blank"
rel="noopener noreferrer"
className="scene-link"
>
<TruncatedText text={scene.title} /> <TruncatedText text={scene.title} />
</a> </ExternalLink>
) : ( ) : (
<TruncatedText text={scene.title} /> <TruncatedText text={scene.title} />
); );
@@ -592,9 +588,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
> >
{scene.urls.map((url) => ( {scene.urls.map((url) => (
<div key={url}> <div key={url}>
<a href={url} target="_blank" rel="noopener noreferrer"> <ExternalLink href={url}>{url}</ExternalLink>
{url}
</a>
</div> </div>
))} ))}
</OptionalField> </OptionalField>
@@ -626,9 +620,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
exclude={excludedFields[fields.stash_ids]} exclude={excludedFields[fields.stash_ids]}
setExclude={(v) => setExcludedField(fields.stash_ids, v)} setExclude={(v) => setExcludedField(fields.stash_ids, v)}
> >
<a href={stashBoxURL} target="_blank" rel="noopener noreferrer"> <ExternalLink href={stashBoxURL}>
{scene.remote_site_id} {scene.remote_site_id}
</a> </ExternalLink>
</OptionalField> </OptionalField>
</div> </div>
); );

View File

@@ -15,6 +15,7 @@ import {
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { TruncatedText } from "src/components/Shared/TruncatedText"; import { TruncatedText } from "src/components/Shared/TruncatedText";
import { excludeFields } from "src/utils/data"; import { excludeFields } from "src/utils/data";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IStudioDetailsProps { interface IStudioDetailsProps {
studio: GQL.ScrapedSceneStudioDataFragment; studio: GQL.ScrapedSceneStudioDataFragment;
@@ -83,15 +84,15 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
); );
} }
function maybeRenderLink() { function maybeRenderStashBoxLink() {
if (!link) return; if (!link) return;
return ( return (
<h6 className="mt-2"> <h6 className="mt-2">
<a href={link} target="_blank" rel="noopener noreferrer"> <ExternalLink href={link}>
<FormattedMessage id="stashbox.source" /> <FormattedMessage id="stashbox.source" />
<Icon icon={faExternalLinkAlt} className="ml-2" /> <Icon icon={faExternalLinkAlt} className="ml-2" />
</a> </ExternalLink>
</h6> </h6>
); );
} }
@@ -104,7 +105,7 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
{maybeRenderField("name", studio.name, !isNew)} {maybeRenderField("name", studio.name, !isNew)}
{maybeRenderField("url", studio.url)} {maybeRenderField("url", studio.url)}
{maybeRenderField("parent_studio", studio.parent?.name, false)} {maybeRenderField("parent_studio", studio.parent?.name, false)}
{maybeRenderLink()} {maybeRenderStashBoxLink()}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -11,6 +11,7 @@ import * as GQL from "src/core/generated-graphql";
import { OptionalField } from "../IncludeButton"; import { OptionalField } from "../IncludeButton";
import { faSave } from "@fortawesome/free-solid-svg-icons"; import { faSave } from "@fortawesome/free-solid-svg-icons";
import { getStashboxBase } from "src/utils/stashbox"; import { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface IStudioName { interface IStudioName {
studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment; studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment;
@@ -21,9 +22,7 @@ interface IStudioName {
const StudioName: React.FC<IStudioName> = ({ studio, id, baseURL }) => { const StudioName: React.FC<IStudioName> = ({ studio, id, baseURL }) => {
const name = const name =
baseURL && id ? ( baseURL && id ? (
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer"> <ExternalLink href={`${baseURL}${id}`}>{studio.name}</ExternalLink>
{studio.name}
</a>
) : ( ) : (
studio.name studio.name
); );

View File

@@ -18,6 +18,7 @@ import {
faImage, faImage,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { objectPath, objectTitle } from "src/core/files"; import { objectPath, objectTitle } from "src/core/files";
import { ExternalLink } from "src/components/Shared/ExternalLink";
interface ITaggerSceneDetails { interface ITaggerSceneDetails {
scene: GQL.SlimSceneDataFragment; scene: GQL.SlimSceneDataFragment;
@@ -174,15 +175,13 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
const stashLinks = scene.stash_ids.map((stashID) => { const stashLinks = scene.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? ( const link = base ? (
<a <ExternalLink
key={`${stashID.endpoint}${stashID.stash_id}`} key={`${stashID.endpoint}${stashID.stash_id}`}
className="small d-block" className="small d-block"
href={`${base}scenes/${stashID.stash_id}`} href={`${base}scenes/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
> >
{stashID.stash_id} {stashID.stash_id}
</a> </ExternalLink>
) : ( ) : (
<div className="small">{stashID.stash_id}</div> <div className="small">{stashID.stash_id}</div>
); );

View File

@@ -27,6 +27,7 @@ import StudioModal from "../scenes/StudioModal";
import { useUpdateStudio } from "../queries"; import { useUpdateStudio } from "../queries";
import { apolloError } from "src/utils"; import { apolloError } from "src/utils";
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
import { ExternalLink } from "src/components/Shared/ExternalLink";
type JobFragment = Pick< type JobFragment = Pick<
GQL.Job, GQL.Job,
@@ -515,14 +516,12 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
if (stashID !== undefined) { if (stashID !== undefined) {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? ( const link = base ? (
<a <ExternalLink
className="small d-block" className="small d-block"
href={`${base}studios/${stashID.stash_id}`} href={`${base}studios/${stashID.stash_id}`}
target="_blank"
rel="noopener noreferrer"
> >
{stashID.stash_id} {stashID.stash_id}
</a> </ExternalLink>
) : ( ) : (
<div className="small">{stashID.stash_id}</div> <div className="small">{stashID.stash_id}</div>
); );

View File

@@ -205,6 +205,15 @@ dd {
} }
} }
.btn.link {
color: $link-color;
&:hover:not(:disabled),
&:active:not(:disabled) {
color: $link-color;
}
}
.detail-header.edit { .detail-header.edit {
background-color: unset; background-color: unset;
overflow: visible; overflow: visible;
@@ -691,12 +700,11 @@ div.dropdown-menu {
flex-wrap: wrap; flex-wrap: wrap;
row-gap: 10px; row-gap: 10px;
/* stylelint-disable */
.badge { .badge {
white-space: normal !important;
margin: unset; margin: unset;
// stylelint-disable declaration-no-important
white-space: normal !important;
} }
/* stylelint-enable */
} }
.tag-item { .tag-item {

View File

@@ -1,5 +1,7 @@
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { ExternalLink } from "src/components/Shared/ExternalLink";
import { TruncatedText } from "src/components/Shared/TruncatedText"; import { TruncatedText } from "src/components/Shared/TruncatedText";
interface ITextField { interface ITextField {
@@ -44,8 +46,8 @@ interface IURLField {
url?: string | null; url?: string | null;
truncate?: boolean | null; truncate?: boolean | null;
target?: string; target?: string;
// use for internal links // an internal link (uses <Link to={url}>)
trusted?: boolean; internal?: boolean;
} }
export const URLField: React.FC<IURLField> = ({ export const URLField: React.FC<IURLField> = ({
@@ -55,11 +57,10 @@ export const URLField: React.FC<IURLField> = ({
url, url,
abbr, abbr,
truncate, truncate,
children, target = "_blank",
target, internal,
trusted,
}) => { }) => {
if (!value && !children) { if (!value) {
return null; return null;
} }
@@ -67,26 +68,30 @@ export const URLField: React.FC<IURLField> = ({
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</> <>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
); );
const rel = !trusted ? "noopener noreferrer" : undefined; function maybeRenderUrl() {
if (!url) return;
const children = truncate ? <TruncatedText text={value} /> : value;
if (internal) {
return (
<Link to={url} target={target}>
{children}
</Link>
);
} else {
return (
<ExternalLink href={url} target={target}>
{children}
</ExternalLink>
);
}
}
return ( return (
<> <>
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt> <dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
<dd> <dd>{maybeRenderUrl()}</dd>
{url ? (
<a href={url} target={target || "_blank"} rel={rel}>
{value ? (
truncate ? (
<TruncatedText text={value} />
) : (
value
)
) : (
children
)}
</a>
) : undefined}
</dd>
</> </>
); );
}; };
@@ -98,8 +103,8 @@ interface IURLsField {
urls?: string[] | null; urls?: string[] | null;
truncate?: boolean | null; truncate?: boolean | null;
target?: string; target?: string;
// use for internal links // an internal link (uses <Link to={url}>)
trusted?: boolean; internal?: boolean;
} }
export const URLsField: React.FC<IURLsField> = ({ export const URLsField: React.FC<IURLsField> = ({
@@ -108,11 +113,10 @@ export const URLsField: React.FC<IURLsField> = ({
urls, urls,
abbr, abbr,
truncate, truncate,
target, target = "_blank",
trusted, internal,
}) => { }) => {
const values = urls ?? []; if (!urls || !urls.length) {
if (!values.length) {
return null; return null;
} }
@@ -120,19 +124,33 @@ export const URLsField: React.FC<IURLsField> = ({
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</> <>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
); );
const rel = !trusted ? "noopener noreferrer" : undefined; const renderUrls = () => {
return urls.map((url, i) => {
if (!url) return;
const children = truncate ? <TruncatedText text={url} /> : url;
if (internal) {
return (
<Link key={i} to={url} target={target}>
{children}
</Link>
);
} else {
return (
<ExternalLink key={i} href={url} target={target}>
{children}
</ExternalLink>
);
}
});
};
return ( return (
<> <>
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt> <dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
<dd> <dd>
<dl> <dl>{renderUrls()}</dl>
{values.map((url, i) => (
<a key={i} href={url} target={target || "_blank"} rel={rel}>
{truncate ? <TruncatedText text={url} /> : url}
</a>
))}
</dl>
</dd> </dd>
</> </>
); );