mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Link improvements and fixes (#4501)
* Add ExternalLink * Replace <a> with <Link>
This commit is contained in:
@@ -9,6 +9,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
|
||||
interface IProps {
|
||||
type: "scene" | "performer";
|
||||
@@ -108,12 +109,12 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
|
||||
<FormattedMessage id="stashbox.submission_successful" />
|
||||
</h6>
|
||||
<div>
|
||||
<a target="_blank" rel="noreferrer noopener" href={reviewUrl}>
|
||||
<ExternalLink href={reviewUrl}>
|
||||
<FormattedMessage
|
||||
id="stashbox.go_review_draft"
|
||||
values={{ endpoint_name: selectedBox?.name }}
|
||||
/>
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface IProps {
|
||||
movie: GQL.MovieDataFragment;
|
||||
@@ -274,15 +275,13 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||
const renderClickableIcons = () => (
|
||||
<span className="name-icons">
|
||||
{movie.url && (
|
||||
<Button className="minimal icon-link" title={movie.url}>
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(movie.url)}
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</a>
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(movie.url)}
|
||||
className="minimal link"
|
||||
title={movie.url}
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface IMovieDetailsPanel {
|
||||
movie: GQL.MovieDataFragment;
|
||||
@@ -34,9 +35,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
|
||||
id="studio"
|
||||
value={
|
||||
movie.studio?.id ? (
|
||||
<a href={`/studios/${movie.studio?.id}`} target="_self">
|
||||
<Link to={`/studios/${movie.studio?.id}`}>
|
||||
{movie.studio?.name}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { DetailImage } from "src/components/Shared/DetailImage";
|
||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface IProps {
|
||||
performer: GQL.PerformerDataFragment;
|
||||
@@ -493,57 +494,50 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
||||
<Icon icon={faHeart} />
|
||||
</Button>
|
||||
{performer.url && (
|
||||
<Button className="minimal icon-link" title={performer.url}>
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(performer.url)}
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</a>
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(performer.url)}
|
||||
className="minimal link"
|
||||
title={performer.url}
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</Button>
|
||||
)}
|
||||
{(urls ?? []).map((url, index) => (
|
||||
<Button key={index} className="minimal icon-link" title={url}>
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(url)}
|
||||
className={`detail-link ${index}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</a>
|
||||
<Button
|
||||
key={index}
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(url)}
|
||||
className={`minimal link detail-link detail-link-${index}`}
|
||||
title={url}
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</Button>
|
||||
))}
|
||||
{performer.twitter && (
|
||||
<Button className="minimal icon-link" title={performer.twitter}>
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(
|
||||
performer.twitter,
|
||||
TextUtils.twitterURL
|
||||
)}
|
||||
className="twitter"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faTwitter} />
|
||||
</a>
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(
|
||||
performer.twitter,
|
||||
TextUtils.twitterURL
|
||||
)}
|
||||
className="minimal link twitter"
|
||||
title={performer.twitter}
|
||||
>
|
||||
<Icon icon={faTwitter} />
|
||||
</Button>
|
||||
)}
|
||||
{performer.instagram && (
|
||||
<Button className="minimal icon-link" title={performer.instagram}>
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(
|
||||
performer.instagram,
|
||||
TextUtils.instagramURL
|
||||
)}
|
||||
className="instagram"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faInstagram} />
|
||||
</a>
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(
|
||||
performer.instagram,
|
||||
TextUtils.instagramURL
|
||||
)}
|
||||
className="minimal link instagram"
|
||||
title={performer.instagram}
|
||||
>
|
||||
<Icon icon={faInstagram} />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Option as SelectOption,
|
||||
} from "../Shared/FilterSelect";
|
||||
import { useCompare } from "src/hooks/state";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export type SelectObject = {
|
||||
id: string;
|
||||
@@ -86,10 +87,9 @@ export const PerformerSelect: React.FC<
|
||||
...optionProps,
|
||||
children: (
|
||||
<span className="react-select-image-option">
|
||||
<a
|
||||
href={`/performers/${object.id}`}
|
||||
<Link
|
||||
to={`/performers/${object.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="performer-select-image-link"
|
||||
>
|
||||
<img
|
||||
@@ -97,7 +97,7 @@ export const PerformerSelect: React.FC<
|
||||
src={object.image_path ?? ""}
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<span>{name}</span>
|
||||
{object.disambiguation && (
|
||||
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>
|
||||
|
||||
@@ -27,22 +27,9 @@
|
||||
color: #ff7373;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: rgb(191, 204, 214);
|
||||
}
|
||||
|
||||
.instagram {
|
||||
color: pink;
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: $btn-padding-y $btn-padding-x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rating-number .form-control {
|
||||
|
||||
@@ -85,7 +85,7 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
|
||||
url={NavUtils.makeScenesPHashMatchUrl(phash?.value)}
|
||||
target="_self"
|
||||
truncate
|
||||
trusted
|
||||
internal
|
||||
/>
|
||||
<URLField
|
||||
id="path"
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useLatestVersion } from "src/core/StashService";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
import { ConstantSetting, SettingGroup } from "./Inputs";
|
||||
import { SettingSection } from "./SettingSection";
|
||||
|
||||
@@ -115,13 +116,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
{ id: "config.about.stash_home" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://github.com/stashapp/stash"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href="https://github.com/stashapp/stash">
|
||||
GitHub
|
||||
</a>
|
||||
</ExternalLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
@@ -131,13 +128,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
{ id: "config.about.stash_wiki" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://docs.stashapp.cc"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href="https://docs.stashapp.cc">
|
||||
Documentation
|
||||
</a>
|
||||
</ExternalLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
@@ -147,13 +140,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
{ id: "config.about.stash_discord" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://discord.gg/2TsNFKt"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href="https://discord.gg/2TsNFKt">
|
||||
Discord
|
||||
</a>
|
||||
</ExternalLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
@@ -163,13 +152,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
{ id: "config.about.stash_open_collective" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://opencollective.com/stashapp"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href="https://opencollective.com/stashapp">
|
||||
Open Collective
|
||||
</a>
|
||||
</ExternalLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs";
|
||||
import { useSettings } from "./context";
|
||||
import { useIntl } from "react-intl";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
|
||||
export const SettingsLibraryPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
@@ -76,13 +77,9 @@ export const SettingsLibraryPanel: React.FC = () => {
|
||||
{intl.formatMessage({
|
||||
id: "config.general.excluded_video_patterns_desc",
|
||||
})}
|
||||
<a
|
||||
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
|
||||
<Icon icon={faQuestionCircle} />
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</span>
|
||||
}
|
||||
value={general.excludes ?? undefined}
|
||||
@@ -98,13 +95,9 @@ export const SettingsLibraryPanel: React.FC = () => {
|
||||
{intl.formatMessage({
|
||||
id: "config.general.excluded_image_gallery_patterns_desc",
|
||||
})}
|
||||
<a
|
||||
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
|
||||
<Icon icon={faQuestionCircle} />
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</span>
|
||||
}
|
||||
value={general.imageExcludes ?? undefined}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
AvailablePluginPackages,
|
||||
InstalledPluginPackages,
|
||||
} from "./PluginPackageManager";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
|
||||
interface IPluginSettingProps {
|
||||
pluginID: string;
|
||||
@@ -97,15 +98,12 @@ export const SettingsPluginsPanel: React.FC = () => {
|
||||
function renderLink(url?: string) {
|
||||
if (url) {
|
||||
return (
|
||||
<Button className="minimal">
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(url)}
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</a>
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(url)}
|
||||
className="minimal link"
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
AvailableScraperPackages,
|
||||
InstalledScraperPackages,
|
||||
} from "./ScraperPackageManager";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
|
||||
interface IURLList {
|
||||
urls: string[];
|
||||
@@ -42,16 +43,7 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
|
||||
const sanitised = TextUtils.sanitiseURL(url);
|
||||
const siteURL = linkSite(sanitised!);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={siteURL}
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{sanitised}
|
||||
</a>
|
||||
);
|
||||
return <ExternalLink href={siteURL}>{sanitised}</ExternalLink>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
min-width: 100px;
|
||||
text-align: right;
|
||||
|
||||
button {
|
||||
.btn {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useHistory } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useSystemStatus, mutateMigrate } from "src/core/StashService";
|
||||
import { migrationNotes } from "src/docs/en/MigrationNotes";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||
|
||||
@@ -35,18 +36,12 @@ export const Migrate: React.FC = () => {
|
||||
: "";
|
||||
|
||||
const discordLink = (
|
||||
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
|
||||
);
|
||||
const githubLink = (
|
||||
<a
|
||||
href="https://github.com/stashapp/stash/issues"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLink href="https://github.com/stashapp/stash/issues">
|
||||
<FormattedMessage id="setup.github_repository" />
|
||||
</a>
|
||||
</ExternalLink>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
faQuestionCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { releaseNotes } from "src/docs/en/ReleaseNotes";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
|
||||
export const Setup: React.FC = () => {
|
||||
const { configuration, loading: configLoading } =
|
||||
@@ -103,18 +104,12 @@ export const Setup: React.FC = () => {
|
||||
}, [configuration]);
|
||||
|
||||
const discordLink = (
|
||||
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
|
||||
);
|
||||
const githubLink = (
|
||||
<a
|
||||
href="https://github.com/stashapp/stash/issues"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ExternalLink href="https://github.com/stashapp/stash/issues">
|
||||
<FormattedMessage id="setup.github_repository" />
|
||||
</a>
|
||||
</ExternalLink>
|
||||
);
|
||||
|
||||
function onConfigLocationChosen(inWorkDir: boolean) {
|
||||
@@ -825,14 +820,9 @@ export const Setup: React.FC = () => {
|
||||
id="setup.success.open_collective"
|
||||
values={{
|
||||
open_collective_link: (
|
||||
<a
|
||||
href="https://opencollective.com/stashapp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{" "}
|
||||
OpenCollective{" "}
|
||||
</a>
|
||||
<ExternalLink href="https://opencollective.com/stashapp">
|
||||
Open Collective
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
5
ui/v2.5/src/components/Shared/ExternalLink.tsx
Normal file
5
ui/v2.5/src/components/Shared/ExternalLink.tsx
Normal 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} />;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { useMemo } from "react";
|
||||
import { StashId } from "src/core/generated-graphql";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { ExternalLink } from "./ExternalLink";
|
||||
|
||||
export type LinkType = "performers" | "scenes" | "studios";
|
||||
|
||||
@@ -26,9 +27,7 @@ export const StashIDPill: React.FC<{
|
||||
return (
|
||||
<span className="stash-id-pill" data-endpoint={endpointName}>
|
||||
<span>{endpointName}</span>
|
||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
||||
{stash_id}
|
||||
</a>
|
||||
<ExternalLink href={link}>{stash_id}</ExternalLink>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface IProps {
|
||||
studio: GQL.StudioDataFragment;
|
||||
@@ -285,15 +286,13 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
||||
const renderClickableIcons = () => (
|
||||
<span className="name-icons">
|
||||
{studio.url && (
|
||||
<Button className="minimal icon-link" title={studio.url}>
|
||||
<a
|
||||
href={TextUtils.sanitiseURL(studio.url)}
|
||||
className="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</a>
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(studio.url)}
|
||||
className="minimal link"
|
||||
title={studio.url}
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||
import { StashIDPill } from "src/components/Shared/StashID";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface IStudioDetailsPanel {
|
||||
studio: GQL.StudioDataFragment;
|
||||
@@ -51,9 +52,9 @@ export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
||||
id="parent_studios"
|
||||
value={
|
||||
studio.parent_studio?.name ? (
|
||||
<a href={`/studios/${studio.parent_studio?.id}`} target="_self">
|
||||
<Link to={`/studios/${studio.parent_studio?.id}`}>
|
||||
{studio.parent_studio.name}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
faExternalLinkAlt,
|
||||
faTimes,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
|
||||
interface IPerformerModalProps {
|
||||
performer: GQL.ScrapedScenePerformerDataFragment;
|
||||
@@ -82,12 +83,14 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||
[name]: !excluded[name],
|
||||
});
|
||||
|
||||
const renderField = (
|
||||
function maybeRenderField(
|
||||
name: string,
|
||||
text: string | null | undefined,
|
||||
truncate: boolean = true
|
||||
) =>
|
||||
text && (
|
||||
) {
|
||||
if (!text) return;
|
||||
|
||||
return (
|
||||
<div className="row no-gutters">
|
||||
<div className="col-5 performer-create-modal-field" key={name}>
|
||||
{!create && (
|
||||
@@ -112,11 +115,72 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base
|
||||
? `${base}performers/${performer.remote_site_id}`
|
||||
: undefined;
|
||||
function maybeRenderImage() {
|
||||
if (!images.length) return;
|
||||
|
||||
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() {
|
||||
if (!performer.name) {
|
||||
@@ -201,89 +265,37 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-7">
|
||||
{renderField("name", performer.name)}
|
||||
{renderField("disambiguation", performer.disambiguation)}
|
||||
{renderField("aliases", performer.aliases)}
|
||||
{renderField(
|
||||
{maybeRenderField("name", performer.name)}
|
||||
{maybeRenderField("disambiguation", performer.disambiguation)}
|
||||
{maybeRenderField("aliases", performer.aliases)}
|
||||
{maybeRenderField(
|
||||
"gender",
|
||||
performer.gender
|
||||
? intl.formatMessage({ id: "gender_types." + performer.gender })
|
||||
: ""
|
||||
)}
|
||||
{renderField("birthdate", performer.birthdate)}
|
||||
{renderField("death_date", performer.death_date)}
|
||||
{renderField("ethnicity", performer.ethnicity)}
|
||||
{renderField("country", getCountryByISO(performer.country))}
|
||||
{renderField("hair_color", performer.hair_color)}
|
||||
{renderField("eye_color", performer.eye_color)}
|
||||
{renderField("height", performer.height)}
|
||||
{renderField("weight", performer.weight)}
|
||||
{renderField("measurements", performer.measurements)}
|
||||
{maybeRenderField("birthdate", performer.birthdate)}
|
||||
{maybeRenderField("death_date", performer.death_date)}
|
||||
{maybeRenderField("ethnicity", performer.ethnicity)}
|
||||
{maybeRenderField("country", getCountryByISO(performer.country))}
|
||||
{maybeRenderField("hair_color", performer.hair_color)}
|
||||
{maybeRenderField("eye_color", performer.eye_color)}
|
||||
{maybeRenderField("height", performer.height)}
|
||||
{maybeRenderField("weight", performer.weight)}
|
||||
{maybeRenderField("measurements", performer.measurements)}
|
||||
{performer?.gender !== GQL.GenderEnum.Male &&
|
||||
renderField("fake_tits", performer.fake_tits)}
|
||||
{renderField("career_length", performer.career_length)}
|
||||
{renderField("tattoos", performer.tattoos, false)}
|
||||
{renderField("piercings", performer.piercings, false)}
|
||||
{renderField("weight", performer.weight, false)}
|
||||
{renderField("details", performer.details)}
|
||||
{renderField("url", performer.url)}
|
||||
{renderField("twitter", performer.twitter)}
|
||||
{renderField("instagram", performer.instagram)}
|
||||
{link && (
|
||||
<h6 className="mt-2">
|
||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
||||
<FormattedMessage id="stashbox.source" />
|
||||
<Icon icon={faExternalLinkAlt} className="ml-2" />
|
||||
</a>
|
||||
</h6>
|
||||
)}
|
||||
maybeRenderField("fake_tits", performer.fake_tits)}
|
||||
{maybeRenderField("career_length", performer.career_length)}
|
||||
{maybeRenderField("tattoos", performer.tattoos, false)}
|
||||
{maybeRenderField("piercings", performer.piercings, false)}
|
||||
{maybeRenderField("weight", performer.weight, false)}
|
||||
{maybeRenderField("details", performer.details)}
|
||||
{maybeRenderField("url", performer.url)}
|
||||
{maybeRenderField("twitter", performer.twitter)}
|
||||
{maybeRenderField("instagram", performer.instagram)}
|
||||
{maybeRenderStashBoxLink()}
|
||||
</div>
|
||||
{images.length > 0 && (
|
||||
<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>
|
||||
)}
|
||||
{maybeRenderImage()}
|
||||
</div>
|
||||
</ModalComponent>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ import PerformerModal from "../PerformerModal";
|
||||
import { useUpdatePerformer } from "../queries";
|
||||
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||
import { mergeStashIDs } from "src/utils/stashbox";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
type JobFragment = Pick<
|
||||
GQL.Job,
|
||||
@@ -466,14 +467,12 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
||||
if (stashID !== undefined) {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
<ExternalLink
|
||||
className="small d-block"
|
||||
href={`${base}performers/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<div className="small">{stashID.stash_id}</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PerformerSelect,
|
||||
} from "src/components/Performers/PerformerSelect";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface IPerformerName {
|
||||
performer: GQL.ScrapedPerformer | Performer;
|
||||
@@ -26,9 +27,7 @@ const PerformerName: React.FC<IPerformerName> = ({
|
||||
}) => {
|
||||
const name =
|
||||
baseURL && id ? (
|
||||
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer">
|
||||
{performer.name}
|
||||
</a>
|
||||
<ExternalLink href={`${baseURL}${id}`}>{performer.name}</ExternalLink>
|
||||
) : (
|
||||
performer.name
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import StudioResult from "./StudioResult";
|
||||
import { useInitialState } from "src/hooks/state";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
const getDurationStatus = (
|
||||
scene: IScrapedScene,
|
||||
@@ -488,14 +489,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
const url = scene.urls?.length ? scene.urls[0] : null;
|
||||
|
||||
const sceneTitleEl = url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="scene-link"
|
||||
>
|
||||
<ExternalLink className="scene-link" href={url}>
|
||||
<TruncatedText text={scene.title} />
|
||||
</a>
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<TruncatedText text={scene.title} />
|
||||
);
|
||||
@@ -592,9 +588,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
>
|
||||
{scene.urls.map((url) => (
|
||||
<div key={url}>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
{url}
|
||||
</a>
|
||||
<ExternalLink href={url}>{url}</ExternalLink>
|
||||
</div>
|
||||
))}
|
||||
</OptionalField>
|
||||
@@ -626,9 +620,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
exclude={excludedFields[fields.stash_ids]}
|
||||
setExclude={(v) => setExcludedField(fields.stash_ids, v)}
|
||||
>
|
||||
<a href={stashBoxURL} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink href={stashBoxURL}>
|
||||
{scene.remote_site_id}
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</OptionalField>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { excludeFields } from "src/utils/data";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface IStudioDetailsProps {
|
||||
studio: GQL.ScrapedSceneStudioDataFragment;
|
||||
@@ -83,15 +84,15 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderLink() {
|
||||
function maybeRenderStashBoxLink() {
|
||||
if (!link) return;
|
||||
|
||||
return (
|
||||
<h6 className="mt-2">
|
||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink href={link}>
|
||||
<FormattedMessage id="stashbox.source" />
|
||||
<Icon icon={faExternalLinkAlt} className="ml-2" />
|
||||
</a>
|
||||
</ExternalLink>
|
||||
</h6>
|
||||
);
|
||||
}
|
||||
@@ -104,7 +105,7 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
|
||||
{maybeRenderField("name", studio.name, !isNew)}
|
||||
{maybeRenderField("url", studio.url)}
|
||||
{maybeRenderField("parent_studio", studio.parent?.name, false)}
|
||||
{maybeRenderLink()}
|
||||
{maybeRenderStashBoxLink()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { OptionalField } from "../IncludeButton";
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface IStudioName {
|
||||
studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment;
|
||||
@@ -21,9 +22,7 @@ interface IStudioName {
|
||||
const StudioName: React.FC<IStudioName> = ({ studio, id, baseURL }) => {
|
||||
const name =
|
||||
baseURL && id ? (
|
||||
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer">
|
||||
{studio.name}
|
||||
</a>
|
||||
<ExternalLink href={`${baseURL}${id}`}>{studio.name}</ExternalLink>
|
||||
) : (
|
||||
studio.name
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
faImage,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { objectPath, objectTitle } from "src/core/files";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
interface ITaggerSceneDetails {
|
||||
scene: GQL.SlimSceneDataFragment;
|
||||
@@ -174,15 +175,13 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
|
||||
const stashLinks = scene.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
<ExternalLink
|
||||
key={`${stashID.endpoint}${stashID.stash_id}`}
|
||||
className="small d-block"
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<div className="small">{stashID.stash_id}</div>
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ import StudioModal from "../scenes/StudioModal";
|
||||
import { useUpdateStudio } from "../queries";
|
||||
import { apolloError } from "src/utils";
|
||||
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
|
||||
type JobFragment = Pick<
|
||||
GQL.Job,
|
||||
@@ -515,14 +516,12 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
|
||||
if (stashID !== undefined) {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
<ExternalLink
|
||||
className="small d-block"
|
||||
href={`${base}studios/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<div className="small">{stashID.stash_id}</div>
|
||||
);
|
||||
|
||||
@@ -205,6 +205,15 @@ dd {
|
||||
}
|
||||
}
|
||||
|
||||
.btn.link {
|
||||
color: $link-color;
|
||||
|
||||
&:hover:not(:disabled),
|
||||
&:active:not(:disabled) {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-header.edit {
|
||||
background-color: unset;
|
||||
overflow: visible;
|
||||
@@ -691,12 +700,11 @@ div.dropdown-menu {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 10px;
|
||||
|
||||
/* stylelint-disable */
|
||||
.badge {
|
||||
white-space: normal !important;
|
||||
margin: unset;
|
||||
// stylelint-disable declaration-no-important
|
||||
white-space: normal !important;
|
||||
}
|
||||
/* stylelint-enable */
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
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";
|
||||
|
||||
interface ITextField {
|
||||
@@ -44,8 +46,8 @@ interface IURLField {
|
||||
url?: string | null;
|
||||
truncate?: boolean | null;
|
||||
target?: string;
|
||||
// use for internal links
|
||||
trusted?: boolean;
|
||||
// an internal link (uses <Link to={url}>)
|
||||
internal?: boolean;
|
||||
}
|
||||
|
||||
export const URLField: React.FC<IURLField> = ({
|
||||
@@ -55,11 +57,10 @@ export const URLField: React.FC<IURLField> = ({
|
||||
url,
|
||||
abbr,
|
||||
truncate,
|
||||
children,
|
||||
target,
|
||||
trusted,
|
||||
target = "_blank",
|
||||
internal,
|
||||
}) => {
|
||||
if (!value && !children) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -67,26 +68,30 @@ export const URLField: React.FC<IURLField> = ({
|
||||
<>{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 (
|
||||
<>
|
||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||
<dd>
|
||||
{url ? (
|
||||
<a href={url} target={target || "_blank"} rel={rel}>
|
||||
{value ? (
|
||||
truncate ? (
|
||||
<TruncatedText text={value} />
|
||||
) : (
|
||||
value
|
||||
)
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</a>
|
||||
) : undefined}
|
||||
</dd>
|
||||
<dd>{maybeRenderUrl()}</dd>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -98,8 +103,8 @@ interface IURLsField {
|
||||
urls?: string[] | null;
|
||||
truncate?: boolean | null;
|
||||
target?: string;
|
||||
// use for internal links
|
||||
trusted?: boolean;
|
||||
// an internal link (uses <Link to={url}>)
|
||||
internal?: boolean;
|
||||
}
|
||||
|
||||
export const URLsField: React.FC<IURLsField> = ({
|
||||
@@ -108,11 +113,10 @@ export const URLsField: React.FC<IURLsField> = ({
|
||||
urls,
|
||||
abbr,
|
||||
truncate,
|
||||
target,
|
||||
trusted,
|
||||
target = "_blank",
|
||||
internal,
|
||||
}) => {
|
||||
const values = urls ?? [];
|
||||
if (!values.length) {
|
||||
if (!urls || !urls.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -120,19 +124,33 @@ export const URLsField: React.FC<IURLsField> = ({
|
||||
<>{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 (
|
||||
<>
|
||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||
<dd>
|
||||
<dl>
|
||||
{values.map((url, i) => (
|
||||
<a key={i} href={url} target={target || "_blank"} rel={rel}>
|
||||
{truncate ? <TruncatedText text={url} /> : url}
|
||||
</a>
|
||||
))}
|
||||
</dl>
|
||||
<dl>{renderUrls()}</dl>
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user