fix(web): tooltips (#1154)

* fix broken tooltips, replace react-tooltip with react-popper-tooltip

* make tooltips use ExternalLink component

* fix import

* get rid of unused import

* fix(tooltip): set delayHide

* removed unecessary comment

* fix tooltip on mobile

* stop tooltip label propagation (mainly for mobile devices)

* added onClick convenience prop for label component wrapper (since onClick isn't propagated down)

---------

Co-authored-by: soup <soup@r4tio.dev>
This commit is contained in:
stacksmash76 2023-10-01 13:16:05 +00:00 committed by GitHub
parent 98df0c9040
commit 3e3454724b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1171 additions and 396 deletions

View file

@ -26,6 +26,7 @@ import {
} from "@components/inputs";
import { clientKeys } from "@screens/settings/DownloadClient";
import { SelectFieldWide } from "@components/inputs/input_wide";
import { DocsLink, ExternalLink } from "@components/ExternalLink";
interface InitialValuesSettings {
basic?: {
@ -54,7 +55,6 @@ interface InitialValues {
settings: InitialValuesSettings;
}
function FormFieldsDeluge() {
const {
values: { tls }
@ -63,11 +63,20 @@ function FormFieldsDeluge() {
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
required
name="host"
label="Host"
help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port"
tooltip={<div><p>See guides for how to connect to Deluge for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated#deluge' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#deluge</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#deluge' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes#deluge</a></div>}
required={true}
tooltip={
<div>
<p>See guides for how to connect to Deluge for various server types in our docs.</p>
<br />
<p>Dedicated servers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#deluge" />
<p>Shared seedbox providers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/shared-seedboxes#deluge" />
</div>
}
/>
<NumberFieldWide
@ -99,14 +108,23 @@ function FormFieldsArr() {
return (
<div className="flex flex-col space-y-4 px-1 mb-4 sm:py-0 sm:space-y-0">
<TextFieldWide
required
name="host"
label="Host"
help="Full url http(s)://domain.ltd and/or subdomain/subfolder"
tooltip={<div><p>See guides for how to connect to the *arr suite for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated/#sonarr' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated/</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#sonarr' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes</a></div>}
required={true}
tooltip={
<div>
<p>See guides for how to connect to the *arr suite for various server types in our docs.</p>
<br />
<p>Dedicated servers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated/#sonarr" />
<p>Shared seedbox providers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/shared-seedboxes#sonarr" />
</div>
}
/>
<PasswordFieldWide name="settings.apikey" label="API key" required={true}/>
<PasswordFieldWide required name="settings.apikey" label="API key" />
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
@ -130,11 +148,20 @@ function FormFieldsQbit() {
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
required
name="host"
label="Host"
help="Eg. http(s)://client.domain.ltd, http(s)://domain.ltd/qbittorrent, http://domain.ltd:port"
tooltip={<div><p>See guides for how to connect to qBittorrent for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated#qbittorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#qbittorrent</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent</a></div>}
required={true}
tooltip={
<div>
<p>See guides for how to connect to qBittorrent for various server types in our docs.</p>
<br />
<p>Dedicated servers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#qbittorrent" />
<p>Shared seedbox providers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent" />
</div>
}
/>
{port > 0 && (
@ -177,16 +204,15 @@ function FormFieldsPorla() {
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
required
name="host"
label="Host"
help="Eg. http(s)://client.domain.ltd, http(s)://domain.ltd/porla, http://domain.ltd:port"
required={true}
/>
<SwitchGroupWide name="tls" label="TLS" />
<PasswordFieldWide name="settings.apikey" label="Auth token" required={true}/>
<PasswordFieldWide required name="settings.apikey" label="Auth token" />
{tls && (
<SwitchGroupWide
@ -215,11 +241,20 @@ function FormFieldsRTorrent() {
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
required
name="host"
label="Host"
help="Eg. http(s)://client.domain.ltd/RPC2, http(s)://domain.ltd/client, http(s)://domain.ltd/RPC2"
tooltip={<div><p>See guides for how to connect to rTorrent for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated#rtorrent--rutorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#rtorrent--rutorrent</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#rtorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes#rtorrent</a></div>}
required={true}
tooltip={
<div>
<p>See guides for how to connect to rTorrent for various server types in our docs.</p>
<br />
<p>Dedicated servers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#rtorrent--rutorrent" />
<p>Shared seedbox providers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/shared-seedboxes#rtorrent" />
</div>
}
/>
<SwitchGroupWide name="tls" label="TLS" />
@ -251,11 +286,20 @@ function FormFieldsTransmission() {
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
required
name="host"
label="Host"
help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd"
tooltip={<div><p>See guides for how to connect to Transmission for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated#transmission' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#transmission</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#transmisison' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes#transmisison</a></div>}
required={true}
tooltip={
<div>
<p>See guides for how to connect to Transmission for various server types in our docs.</p>
<br />
<p>Dedicated servers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#transmission" />
<p>Shared seedbox providers:</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/shared-seedboxes#transmisison" />
</div>
}
/>
<NumberFieldWide name="port" label="Port" help="Port for Transmission" />
@ -286,7 +330,16 @@ function FormFieldsSabnzbd() {
name="host"
label="Host"
help="Eg. http://ip:port or https://url.com/sabnzbd"
// tooltip={<div><p>See guides for how to connect to qBittorrent for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated#qbittorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#qbittorrent</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent</a></div>}
tooltip={
<div>
<p>See our guides on how to connect to qBittorrent for various server types in our docs.</p>
<br />
<p>Dedicated servers:</p>
<ExternalLink href="https://autobrr.com/configuration/download-clients/dedicated#qbittorrent" />
<p>Shared seedbox providers:</p>
<ExternalLink href="https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent" />
</div>
}
/>
{port > 0 && (
@ -357,10 +410,22 @@ function FormFieldsRulesBasic() {
</p>
</div>
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
{settings && settings.rules?.enabled === true && (
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" tooltip={<span><p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p><a href='https://autobrr.com/configuration/download-clients/dedicated#deluge-rules' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#deluge-rules</a><br /><br /><p>See recommendations for various server types here:</p><a href='https://autobrr.com/filters/examples#build-buffer' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/examples#build-buffer</a></span>} />
<NumberFieldWide
name="settings.rules.max_active_downloads"
label="Max active downloads"
tooltip={
<span>
<p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#deluge-rules" />
<br /><br />
<p>See recommendations for various server types here:</p>
<DocsLink href='https://autobrr.com/filters/examples#build-buffer' />
</span>
}
/>
)}
</div>
);
@ -389,7 +454,16 @@ function FormFieldsRulesQbit() {
<NumberFieldWide
name="settings.rules.max_active_downloads"
label="Max active downloads"
tooltip={<><p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p><a href='https://autobrr.com/configuration/download-clients/dedicated#qbittorrent-rules' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#qbittorrent-rules</a><br /><br /><p>See recommendations for various server types here:</p><a href='https://autobrr.com/filters/examples#build-buffer' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/examples#build-buffer</a></>} />
tooltip={
<>
<p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#qbittorrent-rules" />
<br /><br />
<p>See recommendations for various server types here:</p>
<DocsLink href="https://autobrr.com/filters/examples#build-buffer" />
</>
}
/>
<SwitchGroupWide
name="settings.rules.ignore_slow_torrents"
label="Ignore slow torrents"
@ -447,7 +521,15 @@ function FormFieldsRulesTransmission() {
<NumberFieldWide
name="settings.rules.max_active_downloads"
label="Max active downloads"
tooltip={<><p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p><a href='https://autobrr.com/configuration/download-clients/dedicated#transmission-rules' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#transmission-rules</a><br /><br /><p>See recommendations for various server types here:</p><a href='https://autobrr.com/filters/examples#build-buffer' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/examples#build-buffer</a></>}
tooltip={
<>
<p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p>
<DocsLink href="https://autobrr.com/configuration/download-clients/dedicated#transmission-rules" />
<br /><br />
<p>See recommendations for various server types here:</p>
<DocsLink href="https://autobrr.com/filters/examples#build-buffer" />
</>
}
/>
</>
)}
@ -456,11 +538,11 @@ function FormFieldsRulesTransmission() {
}
export const rulesComponentMap: componentMapType = {
DELUGE_V1: <FormFieldsRulesBasic/>,
DELUGE_V2: <FormFieldsRulesBasic/>,
QBITTORRENT: <FormFieldsRulesQbit/>,
PORLA: <FormFieldsRulesBasic/>,
TRANSMISSION: <FormFieldsRulesTransmission/>
DELUGE_V1: <FormFieldsRulesBasic />,
DELUGE_V2: <FormFieldsRulesBasic />,
QBITTORRENT: <FormFieldsRulesQbit />,
PORLA: <FormFieldsRulesBasic />,
TRANSMISSION: <FormFieldsRulesTransmission />
};
interface formButtonsProps {
@ -582,12 +664,12 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
mutationFn: (client: DownloadClient) => APIClient.download_clients.create(client),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
toast.custom((t) => <Toast type="success" body="Client was added" t={t}/>);
toast.custom((t) => <Toast type="success" body="Client was added" t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t}/>);
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t} />);
}
});
@ -647,7 +729,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/>
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
@ -697,8 +779,8 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
</div>
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide name="name" label="Name" required={true}/>
<SwitchGroupWide name="enabled" label="Enabled"/>
<TextFieldWide required name="name" label="Name" />
<SwitchGroupWide name="enabled" label="Enabled" />
<RadioFieldsetWide
name="type"
legend="Type"
@ -720,7 +802,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
values={values}
/>
<DEBUG values={values}/>
<DEBUG values={values} />
</Form>
)}
</Formik>
@ -756,7 +838,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) });
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t} />);
toggle();
}
});
@ -769,7 +851,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
queryClient.invalidateQueries({ queryKey: clientKeys.lists() });
queryClient.invalidateQueries({ queryKey: clientKeys.detail(client.id) });
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>);
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t} />);
toggleDeleteModal();
}
});
@ -841,7 +923,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
text="Are you sure you want to remove this download client? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/>
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
@ -892,8 +974,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" required={true}/>
<SwitchGroupWide name="enabled" label="Enabled"/>
<TextFieldWide required name="name" label="Name" />
<SwitchGroupWide name="enabled" label="Enabled" />
<RadioFieldsetWide
name="type"
legend="Type"
@ -916,7 +998,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP
values={values}
/>
<DEBUG values={values}/>
<DEBUG values={values} />
</Form>
);
}}

View file

@ -22,6 +22,7 @@ import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/selec
import { FeedDownloadTypeOptions } from "@domain/constants";
import { feedKeys } from "@screens/settings/Feed";
import { indexerKeys } from "@screens/settings/Indexer";
import { DocsLink } from "@components/ExternalLink";
const Input = (props: InputProps) => (
<components.Input
@ -86,13 +87,29 @@ const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
{ind.irc.settings.map((f: IndexerSetting, idx: number) => {
switch (f.type) {
case "text":
return <TextFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} autoComplete="off" validate={validateField(f)} tooltip={<div><p>Please read our IRC guide if you are unfamiliar with IRC.</p><a href='https://autobrr.com/configuration/irc' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/irc</a></div>} />;
case "secret":
if (f.name === "invite_command") {
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultVisible={true} defaultValue={f.default} validate={validateField(f)} />;
}
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
case "text":
return (
<TextFieldWide
key={idx}
name={`irc.${f.name}`}
label={f.label}
required={f.required}
help={f.help}
autoComplete="off"
validate={validateField(f)}
tooltip={
<div>
<p>Please read our IRC guide if you are unfamiliar with IRC.</p>
<DocsLink href="https://autobrr.com/configuration/irc" />
</div>
}
/>
);
case "secret":
if (f.name === "invite_command") {
return <PasswordFieldWide defaultVisible name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
}
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
}
return null;
})}
@ -224,7 +241,21 @@ const SettingFields = (ind: IndexerDefinition, indexer: string) => {
);
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} validate={validateField(f)} tooltip={<div><p>This field does not take a full URL. Only use alphanumeric strings like <code>uqcdi67cibkx3an8cmdm</code>.</p><br /><a href='https://autobrr.com/faqs#common-action-rejections' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/faqs#common-action-rejections</a></div>} />
<PasswordFieldWide
name={`settings.${f.name}`}
label={f.label}
required={f.required}
key={idx}
help={f.help}
validate={validateField(f)}
tooltip={
<div>
<p>This field does not take a full URL. Only use alphanumeric strings like <code>uqcdi67cibkx3an8cmdm</code>.</p>
<br />
<DocsLink href="https://autobrr.com/faqs#common-action-rejections" />
</div>
}
/>
);
}
return null;
@ -752,14 +783,26 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
<div key="opt">
{settings.map((f: IndexerSetting, idx: number) => {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
);
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} tooltip={<div><p>This field does not take a full URL. Only use alphanumeric strings like <code>uqcdi67cibkx3an8cmdm</code>.</p><br /><a href='https://autobrr.com/faqs#common-action-rejections' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/faqs#common-action-rejections</a></div>} />
);
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
);
case "secret":
return (
<PasswordFieldWide
key={idx}
name={`settings.${f.name}`}
label={f.label}
help={f.help}
tooltip={
<div>
<p>This field does not take a full URL. Only use alphanumeric strings like <code>uqcdi67cibkx3an8cmdm</code>.</p>
<br />
<DocsLink href="https://autobrr.com/faqs#common-action-rejections" />
</div>
}
/>
);
}
return null;
})}

View file

@ -20,6 +20,7 @@ import Toast from "@components/notifications/Toast";
import { SlideOver } from "@components/panels";
import { componentMapType } from "./DownloadClientForms";
import { notificationKeys } from "@screens/settings/Notifications";
import { ExternalLink } from "@components/ExternalLink";
const Input = (props: InputProps) => {
return (
@ -68,7 +69,14 @@ function FormFieldsDiscord() {
<div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Create a <a href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400">webhook integration</a> in your server.
{"Create a "}
<ExternalLink
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400"
>
webhook integration
</ExternalLink>
{" in your server."}
</p>
</div>
@ -107,7 +115,14 @@ function FormFieldsTelegram() {
<div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Read how to <a href="https://core.telegram.org/bots#3-how-do-i-create-a-bot" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400">create a bot</a>.
{"Read how to "}
<ExternalLink
href="https://core.telegram.org/bots#3-how-do-i-create-a-bot"
className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400"
>
create a bot
</ExternalLink>
{"."}
</p>
</div>
@ -136,7 +151,14 @@ function FormFieldsPushover() {
<div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Settings</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Register a new <a href="https://support.pushover.net/i175-how-do-i-get-an-api-or-application-token" rel="noopener noreferrer" target="_blank" className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400">application</a> and add its API Token here.
{"Register a new "}
<ExternalLink
href="https://support.pushover.net/i175-how-do-i-get-an-api-or-application-token"
className="font-medium text-blue-500 underline underline-offset-1 hover:text-blue-400"
>
application
</ExternalLink>
{" and add its API Token here."}
</p>
</div>