diff --git a/i18n/de/code.json b/i18n/de/code.json index bb9c7a477cf..e5b58ff1062 100644 --- a/i18n/de/code.json +++ b/i18n/de/code.json @@ -481,5 +481,29 @@ }, "Privacy": { "message": "Datenschutz" + }, + "theme.common.sdk.placeholder": { + "message": "Ihr SDK" + }, + "theme.common.connector.placeholder": { + "message": "Ihr Anbieter" + }, + "theme.common.sdk.native": { + "message": "Nativ" + }, + "theme.common.sdk.spa": { + "message": "Single Page" + }, + "theme.common.sdk.traditional": { + "message": "Traditionelles Web" + }, + "theme.common.connector.social": { + "message": "Soziale Anbieter" + }, + "theme.common.connector.email": { + "message": "E-Mail-Anbieter" + }, + "theme.common.connector.sms": { + "message": "SMS-Anbieter" } } diff --git a/i18n/es/code.json b/i18n/es/code.json index 60b5ef4778c..b8c8b3d6d0d 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -481,5 +481,29 @@ }, "Privacy": { "message": "Privacidad" + }, + "theme.common.sdk.placeholder": { + "message": "Tu SDK" + }, + "theme.common.connector.placeholder": { + "message": "Tu proveedor" + }, + "theme.common.sdk.native": { + "message": "Nativo" + }, + "theme.common.sdk.spa": { + "message": "Página única" + }, + "theme.common.sdk.traditional": { + "message": "Web tradicional" + }, + "theme.common.connector.social": { + "message": "Proveedores sociales" + }, + "theme.common.connector.email": { + "message": "Proveedores de correo electrónico" + }, + "theme.common.connector.sms": { + "message": "Proveedores de SMS" } } diff --git a/i18n/fr/code.json b/i18n/fr/code.json index 26f6d0b0db5..bbc6b582315 100644 --- a/i18n/fr/code.json +++ b/i18n/fr/code.json @@ -481,5 +481,29 @@ }, "Privacy": { "message": "Confidentialité" + }, + "theme.common.sdk.placeholder": { + "message": "Votre SDK" + }, + "theme.common.connector.placeholder": { + "message": "Votre fournisseur" + }, + "theme.common.sdk.native": { + "message": "Natif" + }, + "theme.common.sdk.spa": { + "message": "Page unique" + }, + "theme.common.sdk.traditional": { + "message": "Web traditionnel" + }, + "theme.common.connector.social": { + "message": "Fournisseurs sociaux" + }, + "theme.common.connector.email": { + "message": "Fournisseurs d'e-mail" + }, + "theme.common.connector.sms": { + "message": "Fournisseurs de SMS" } } diff --git a/i18n/ja/code.json b/i18n/ja/code.json index 9949daac602..4352c91ea6a 100644 --- a/i18n/ja/code.json +++ b/i18n/ja/code.json @@ -1,6 +1,6 @@ { "theme.languageSwitchBanner.message": { - "message": "A version matching your device language is available. Switch to ", + "message": "お使いのデバイスの言語に一致するバージョンが利用可能です。切り替え先 ", "description": "The prompt message displayed in the language switch banner" }, "theme.docs.DocCard.categoryDescription.plurals": { @@ -8,42 +8,45 @@ "description": "The default description for a category card in the generated index about how many items this category includes" }, "theme.common.doYouFindThisPageHelpful": { - "message": "Do you find this page helpful?", + "message": "このページは役に立ちましたか?", "description": "The label for the docs helpfulness question" }, "theme.common.yes": { - "message": "Yes" + "message": "はい", + "description": "The label for the docs helpful button" }, "theme.common.no": { - "message": "No" + "message": "いいえ", + "description": "The label for the docs not helpful button" }, "theme.common.thanksForTheFeedback": { - "message": "Thank you for helping improve Logto Docs! 💜 ", + "message": "Logtoドキュメントの改善にご協力いただきありがとうございます!💜", "description": "The success message after submitting feedback" }, "theme.common.feedbackPlaceholder": { - "message": "We'd love to hear your feedback!", + "message": "フィードバックをお聞かせください!", "description": "The placeholder of the feedback textarea" }, "theme.common.submit": { - "message": "Submit", + "message": "送信", "description": "The label of the submit button" }, "theme.common.helpUsImproveTheDocs": { - "message": "Help us improve the docs!" + "message": "ドキュメントの改善にご協力ください!", + "description": "The label for the edit this page button" }, "theme.common.editThisPage": { "message": "このページを編集", "description": "The link label to edit the current page" }, "Hosted in 🇪🇺🇺🇸🇦🇺": { - "message": "Hosted in 🇪🇺🇺🇸🇦🇺" + "message": "🇪🇺🇺🇸🇦🇺 でホストされています" }, "Terms": { - "message": "Terms" + "message": "利用規約" }, "Privacy": { - "message": "Privacy" + "message": "プライバシー" }, "theme.DocSidebarItem.expandCategoryAriaLabel": { "message": "'{label}'の目次を開く", @@ -478,5 +481,29 @@ "theme.tags.tagsPageTitle": { "message": "タグ", "description": "The title of the tag list page" + }, + "theme.common.sdk.placeholder": { + "message": "SDKを選択" + }, + "theme.common.connector.placeholder": { + "message": "プロバイダーを選択" + }, + "theme.common.sdk.native": { + "message": "ネイティブ" + }, + "theme.common.sdk.spa": { + "message": "シングルページ" + }, + "theme.common.sdk.traditional": { + "message": "従来のウェブアプリ" + }, + "theme.common.connector.social": { + "message": "ソーシャルプロバイダー" + }, + "theme.common.connector.email": { + "message": "メールプロバイダー" + }, + "theme.common.connector.sms": { + "message": "SMSプロバイダー" } } diff --git a/i18n/pt-BR/code.json b/i18n/pt-BR/code.json index cd4adb18344..908b8822b68 100644 --- a/i18n/pt-BR/code.json +++ b/i18n/pt-BR/code.json @@ -481,5 +481,29 @@ }, "Privacy": { "message": "Privacidade" + }, + "theme.common.sdk.placeholder": { + "message": "Seu SDK" + }, + "theme.common.connector.placeholder": { + "message": "Seu provedor" + }, + "theme.common.sdk.native": { + "message": "Nativo" + }, + "theme.common.sdk.spa": { + "message": "Página única" + }, + "theme.common.sdk.traditional": { + "message": "Web tradicional" + }, + "theme.common.connector.social": { + "message": "Provedores sociais" + }, + "theme.common.connector.email": { + "message": "Provedores de e-mail" + }, + "theme.common.connector.sms": { + "message": "Provedores de SMS" } } diff --git a/i18n/zh-CN/code.json b/i18n/zh-CN/code.json index ad8792c5e8a..afd1aed2129 100644 --- a/i18n/zh-CN/code.json +++ b/i18n/zh-CN/code.json @@ -481,5 +481,29 @@ }, "Privacy": { "message": "隐私" + }, + "theme.common.sdk.placeholder": { + "message": "选择 SDK" + }, + "theme.common.connector.placeholder": { + "message": "选择提供商" + }, + "theme.common.sdk.native": { + "message": "原生" + }, + "theme.common.sdk.spa": { + "message": "单页应用" + }, + "theme.common.sdk.traditional": { + "message": "传统网页" + }, + "theme.common.connector.social": { + "message": "社交提供商" + }, + "theme.common.connector.email": { + "message": "邮件提供商" + }, + "theme.common.connector.sms": { + "message": "短信提供商" } } diff --git a/plugins/tutorial-generator/index.ts b/plugins/tutorial-generator/index.ts index 77b2f89802a..e696691be91 100644 --- a/plugins/tutorial-generator/index.ts +++ b/plugins/tutorial-generator/index.ts @@ -7,6 +7,8 @@ import type { DocMetadata, LoadedContent } from '@docusaurus/plugin-content-docs import { type PluginConfig } from '@docusaurus/types'; import { type Optional } from '@silverhand/essentials'; +import { getConnectorDisplayName, getConnectorPath, getSdkDisplayName, getSdkPath } from './utils'; + type DocGroups = { sdks: DocMetadata[]; socialConnectors: DocMetadata[]; @@ -16,6 +18,9 @@ type DocGroups = { const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const getCurrentLocale = ({ permalink, slug }: DocMetadata) => + permalink === slug ? undefined : permalink.split('/')[1]; + /** * A helper function to get the absolute path of a doc. * @@ -30,8 +35,7 @@ const getAbsoluteDocDir = (doc: DocMetadata) => { }; const getAbsoluteOutputDir = (doc: DocMetadata) => { - const { permalink, slug } = doc; - const locale = permalink === slug ? undefined : permalink.split('/')[1]; + const locale = getCurrentLocale(doc); const relativeOutputPath = locale ? `i18n/${locale}/docusaurus-plugin-content-blog-tutorial/build-with-logto` : 'tutorial/build-with-logto'; @@ -55,6 +59,7 @@ const tutorialGenerator: PluginConfig = () => { } const outputDir = getAbsoluteOutputDir(docs[0]); + const locale = getCurrentLocale(docs[0]); const socialTemplatePath = path.join(outputDir, '_template-social.mdx'); const passwordlessTemplatePath = path.join(outputDir, '_template-passwordless.mdx'); @@ -69,7 +74,7 @@ const tutorialGenerator: PluginConfig = () => { fs.readFile(passwordlessTemplatePath, 'utf8'), ]); - const { sdks, socialConnectors, emailConnectors, smsConnectors } = docs.reduce( + const tutorialMetadata = docs.reduce( (acc, doc) => { const { sourceDirName } = doc; const absoluteDocDir = getAbsoluteDocDir(doc); @@ -108,6 +113,16 @@ const tutorialGenerator: PluginConfig = () => { } ); + if (!locale) { + // Write tutorial metadata of default locale to output folder as json + await fs.writeFile( + path.join(outputDir, 'metadata.json'), + JSON.stringify(tutorialMetadata, null, 2) + ); + } + + const { sdks, socialConnectors, emailConnectors, smsConnectors } = tutorialMetadata; + // Copy assets folders to output directory const assetsDir = path.join(__dirname, './assets'); const targetAssetsDir = path.join(outputDir, 'assets'); @@ -133,20 +148,12 @@ const tutorialGenerator: PluginConfig = () => { await Promise.all( sdks.map((sdk) => connectors.map(async (connector) => { - const connectorName = String( - connector.frontMatter.tutorial_name ?? connector.frontMatter.sidebar_label ?? '' - ); - const connectorPath = connectorName.replaceAll(' ', '-').toLowerCase(); - const sdkPath = - String(sdk.frontMatter.tutorial_name ?? '') - .replaceAll(' ', '-') - .replaceAll(/[()]/g, '') - .replaceAll('.', 'dot') - .toLowerCase() || sdk.slug.split('/').slice(2).join('-'); + const connectorPath = getConnectorPath(connector); + const sdkPath = getSdkPath(sdk); /* eslint-disable no-template-curly-in-string */ const post = template - .replaceAll('${connector}', connectorName) + .replaceAll('${connector}', getConnectorDisplayName(connector)) .replaceAll('${connectorPath}', connectorPath) .replaceAll( '${connectorConfigName}', @@ -160,10 +167,7 @@ const tutorialGenerator: PluginConfig = () => { ) .replaceAll('${connectorDocDir}', getRelativeDocSourcePath(connector)) .replaceAll('${sdkDocDir}', getRelativeDocSourcePath(sdk)) - .replaceAll( - '${sdk}', - String(sdk.frontMatter.tutorial_name ?? sdk.frontMatter.sidebar_label) - ) + .replaceAll('${sdk}', getSdkDisplayName(sdk)) .replaceAll('${sdkPath}', sdkPath) .replaceAll('${sdkOfficialLink}', String(sdk.frontMatter.official_link)) .replaceAll('${language}', String(sdk.frontMatter.language)) diff --git a/plugins/tutorial-generator/utils.ts b/plugins/tutorial-generator/utils.ts new file mode 100644 index 00000000000..e21fc2dc049 --- /dev/null +++ b/plugins/tutorial-generator/utils.ts @@ -0,0 +1,23 @@ +import { type DocMetadata } from '@docusaurus/plugin-content-docs'; + +export const getSdkDisplayName = (sdk: DocMetadata) => + String(sdk.frontMatter.tutorial_name ?? sdk.frontMatter.sidebar_label ?? ''); + +export const getConnectorDisplayName = (connector: DocMetadata) => + String(connector.frontMatter.tutorial_name ?? connector.frontMatter.sidebar_label ?? ''); + +export const getSdkPath = (metadata: DocMetadata) => { + const sdkName = String(metadata.frontMatter.tutorial_name ?? ''); + + return sdkName + ? sdkName.replaceAll(' ', '-').replaceAll(/[()]/g, '').replaceAll('.', 'dot').toLowerCase() + : metadata.slug.split('/').slice(2).join('-'); +}; + +export const getConnectorPath = (metadata: DocMetadata) => { + const connectorName = String( + metadata.frontMatter.tutorial_name ?? metadata.frontMatter.sidebar_label ?? '' + ); + + return connectorName.replaceAll(' ', '-').toLowerCase(); +}; diff --git a/src/components/Dropdown/index.tsx b/src/components/Dropdown/index.tsx index 879e28b75fe..3948a224cba 100644 --- a/src/components/Dropdown/index.tsx +++ b/src/components/Dropdown/index.tsx @@ -12,6 +12,8 @@ import styles from './index.module.scss'; export { default as DropdownItem } from './DropdownItem'; +ReactModal.setAppElement('#__docusaurus'); + type Props = { readonly children: ReactNode; readonly isOpen: boolean; diff --git a/src/hooks/use-categorized-tutorial-metadata.ts b/src/hooks/use-categorized-tutorial-metadata.ts new file mode 100644 index 00000000000..01df481aa16 --- /dev/null +++ b/src/hooks/use-categorized-tutorial-metadata.ts @@ -0,0 +1,62 @@ +import { type DocMetadata } from '@docusaurus/plugin-content-docs'; + +import metadata from '@site/tutorial/build-with-logto/metadata.json'; + +type DocGroups = { + sdks: DocMetadata[]; + socialConnectors: DocMetadata[]; + emailConnectors: DocMetadata[]; + smsConnectors: DocMetadata[]; +}; + +/** + * Matches the `app_type` frontmatter value of an SDK doc in quick-starts. + */ +export enum DocAppType { + Native = 'Native app', + Traditional = 'Traditional web', + SPA = 'Single page app', +} + +const useCategorizedTutorialMetadata = () => { + // eslint-disable-next-line no-restricted-syntax + const { sdks, socialConnectors, emailConnectors, smsConnectors } = metadata as DocGroups; + + const { nativeSdks, traditionalSdks, spaSdks } = sdks.reduce<{ + nativeSdks: DocMetadata[]; + traditionalSdks: DocMetadata[]; + spaSdks: DocMetadata[]; + }>( + (acc, sdk) => { + const appType = sdk.frontMatter.app_type; + switch (appType) { + case DocAppType.Native: { + return { ...acc, nativeSdks: [...acc.nativeSdks, sdk] }; + } + case DocAppType.Traditional: { + return { ...acc, traditionalSdks: [...acc.traditionalSdks, sdk] }; + } + case DocAppType.SPA: { + return { ...acc, spaSdks: [...acc.spaSdks, sdk] }; + } + default: { + return acc; + } + } + }, + { nativeSdks: [], traditionalSdks: [], spaSdks: [] } + ); + + return { + allSdks: sdks, + allConnectors: [...socialConnectors, ...emailConnectors, ...smsConnectors], + nativeSdks, + traditionalSdks, + spaSdks, + socialConnectors, + emailConnectors, + smsConnectors, + }; +}; + +export default useCategorizedTutorialMetadata; diff --git a/src/theme/BlogLayout/index.tsx b/src/theme/BlogLayout/index.tsx index 6db4adb7130..cd5c110191e 100644 --- a/src/theme/BlogLayout/index.tsx +++ b/src/theme/BlogLayout/index.tsx @@ -2,6 +2,9 @@ import type { Props } from '@theme/BlogLayout'; import BlogSidebar from '@theme/BlogSidebar'; import Layout from '@theme/Layout'; import clsx from 'clsx'; +import ReactModal from 'react-modal'; + +ReactModal.setAppElement('#__docusaurus'); export default function BlogLayout(props: Props): JSX.Element { const { sidebar, toc, children, ...layoutProps } = props; diff --git a/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss b/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss new file mode 100644 index 00000000000..7412995f782 --- /dev/null +++ b/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss @@ -0,0 +1,62 @@ +.overlay { + background: transparent; + position: fixed; + inset: 0; + z-index: 199; +} + +.dropdown { + position: absolute; + min-width: 650px; + padding: 28px 24px; + background: #fff; + border-radius: 8px; + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 20%); + + label { + display: block; + font-size: 14px; + line-height: 20px; + font-weight: 700; + letter-spacing: 1.4px; + color: var(--logto-color-text-secondary); + margin-bottom: 8px; + position: relative; + } +} + +.dropdownGroup { + display: grid; + grid-template-columns: repeat(3, 1fr); + + .dropdownItem { + display: flex; + padding: 6px 12px; + font: var(--font-body-2); + color: var(--logto-color-text); + border-radius: 6px; + cursor: pointer; + + &:hover { + background: var(--logto-hover); + } + } + + + label { + margin-top: 33px; + + &::before { + content: ''; + position: absolute; + border-top: 1px solid var(--logto-line-divider); + top: -16px; + inset-inline: 0; + } + } +} + +html[data-theme='dark'] { + .dropdown { + background: var(--logto-container-neutral-bg); + } +} \ No newline at end of file diff --git a/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx b/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx new file mode 100644 index 00000000000..6075b95a023 --- /dev/null +++ b/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx @@ -0,0 +1,83 @@ +import { type DocMetadata } from '@docusaurus/plugin-content-docs'; +import { type Nullable } from '@silverhand/essentials'; +import { Fragment, type RefObject, useRef } from 'react'; +import ReactModal from 'react-modal'; + +import { usePosition } from '@site/src/hooks/use-position'; +import { onKeyDownHandler } from '@site/src/utils/a11y'; + +import styles from './index.module.scss'; + +type DropdownProps = { + readonly isOpen: boolean; + readonly anchorRef: RefObject>; + readonly options: Record; + readonly onSelect: (option: { displayName: string; metadata: DocMetadata }) => void; + readonly onClose: () => void; +}; + +const SelectionDropdown = ({ isOpen, anchorRef, options, onSelect, onClose }: DropdownProps) => { + const overlayRef = useRef(null); + + const { position, mutate } = usePosition({ + verticalAlign: 'bottom', + horizontalAlign: 'start', + offset: { vertical: 6, horizontal: 0 }, + anchorRef, + overlayRef, + }); + + return ( + +
+ {Object.entries(options).map(([groupLabel, categorizedGuides]) => { + return ( + + +
+ {categorizedGuides.map((metadata) => { + const { frontMatter, id } = metadata; + const displayName = String( + frontMatter.tutorial_name ?? frontMatter.sidebar_label ?? '' + ); + return ( +
{ + onSelect({ displayName, metadata }); + })} + onClick={() => { + onSelect({ displayName, metadata }); + }} + > + {displayName} +
+ ); + })} +
+
+ ); + })} +
+
+ ); +}; + +export default SelectionDropdown; diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx index 9d2582b41ab..cbd0d192110 100644 --- a/src/theme/BlogPostItem/Header/Title/index.tsx +++ b/src/theme/BlogPostItem/Header/Title/index.tsx @@ -6,6 +6,8 @@ import type { Props } from '@theme/BlogPostItem/Header/Title'; import { clsx } from 'clsx'; import { useCallback, useEffect } from 'react'; +import TitleWithSelectionDropdown from '../TitleWithSelectionDropdown'; + import styles from './styles.module.css'; const Content = ({ className }: Props): JSX.Element => { @@ -71,7 +73,7 @@ const Content = ({ className }: Props): JSX.Element => { itemProp="headline" > {isBlogPostPage ? ( - title + ) : ( {title} diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css index 5edf1f07974..c08d14098f5 100644 --- a/src/theme/BlogPostItem/Header/Title/styles.module.css +++ b/src/theme/BlogPostItem/Header/Title/styles.module.css @@ -1,14 +1,6 @@ .title { - font-size: 3rem; -} - -/** - Blog post title should be smaller on smaller devices -**/ -@media (max-width: 576px) { - .title { - font-size: 2rem; - } + font: var(--font-headline-1); + margin-top: 32px; } /* Make it smaller while in the list */ diff --git a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss index 94bc4d49a46..47e46be5048 100644 --- a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss +++ b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss @@ -21,6 +21,11 @@ transform: rotate(0deg); } } + + .placeholder { + color: var(--logto-color-placeholder); + user-select: none; + } } /* Hide dropdown anchors from title on smaller devices */ diff --git a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx new file mode 100644 index 00000000000..c43a1853f61 --- /dev/null +++ b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx @@ -0,0 +1,227 @@ +import Translate, { translate } from '@docusaurus/Translate'; +import { type PropBlogPostMetadata } from '@docusaurus/plugin-content-blog'; +import { type DocMetadata } from '@docusaurus/plugin-content-docs'; +import { useHistory } from '@docusaurus/router'; +import { clsx } from 'clsx'; +import { useMemo, useRef, useState } from 'react'; + +import { + getConnectorDisplayName, + getConnectorPath, + getSdkDisplayName, + getSdkPath, +} from '@site/plugins/tutorial-generator/utils'; +import useCategorizedTutorialMetadata from '@site/src/hooks/use-categorized-tutorial-metadata'; +import { onKeyDownHandler } from '@site/src/utils/a11y'; + +import Dropdown from '../SelectionDropdown'; + +import styles from './index.module.scss'; + +type Props = { + readonly title: string; + readonly metadata: PropBlogPostMetadata; + readonly isInListView?: boolean; + readonly defaultSdkSlugPart?: string; + readonly defaultConnectorSlugPart?: string; + readonly onSelectSdk?: (docMetadata: DocMetadata) => void; + readonly onSelectConnector?: (docMetadata: DocMetadata) => void; +}; + +type DropdownType = 'sdk' | 'connector'; + +const slugFirstPart = 'how-to-build-'; +const slugMiddlePart = '-sign-in-with'; +const slugLastPart = '-and-logto'; + +/** + * Escape potential parentheses in the SDK / connector name. + * The result will be used for the regex that splits the title into parts. + */ +const normalizeName = (name: string) => name.replaceAll('(', '\\(').replaceAll(')', '\\)'); + +const TitleWithSelectionDropdown = ({ + title, + metadata, + isInListView, + defaultSdkSlugPart, + defaultConnectorSlugPart, + onSelectSdk, + onSelectConnector, +}: Props) => { + const history = useHistory(); + const slug = metadata.frontMatter.slug ?? ''; + const sdkName = String(metadata.frontMatter.sdk ?? ''); + const connectorName = String(metadata.frontMatter.connector ?? ''); + const [isDropdownOpen, setIsDropdownOpen] = useState(); + + const allTutorialsMetadata = useCategorizedTutorialMetadata(); + + const sdkNameRef = useRef(null); + const connectorNameRef = useRef(null); + + const defaultSdk = useMemo(() => { + if (!defaultSdkSlugPart) { + return; + } + return allTutorialsMetadata.allSdks.find((data) => getSdkPath(data) === defaultSdkSlugPart); + }, [defaultSdkSlugPart, allTutorialsMetadata.allSdks]); + + const defaultConnector = useMemo(() => { + if (!defaultConnectorSlugPart) { + return; + } + return allTutorialsMetadata.allConnectors.find( + (data) => getConnectorPath(data) === defaultConnectorSlugPart + ); + }, [defaultConnectorSlugPart, allTutorialsMetadata.allConnectors]); + + if (!sdkName && !connectorName) { + return title; + } + + const titleParts = title + .split(new RegExp(`(${normalizeName(sdkName)}|${normalizeName(connectorName)})`, 'g')) + .filter(Boolean); + + const showDropdown = (type: DropdownType) => { + setIsDropdownOpen(type); + }; + + const onSelectDropdown = (option: { + type: DropdownType; + displayName: string; + metadata: DocMetadata; + }) => { + const { type, displayName, metadata } = option; + const onSelectFn = type === 'sdk' ? onSelectSdk : onSelectConnector; + const getPathFn = type === 'sdk' ? getSdkPath : getConnectorPath; + const elementRef = type === 'sdk' ? sdkNameRef : connectorNameRef; + + onSelectFn?.(metadata); + setIsDropdownOpen(undefined); + + if (elementRef.current) { + // eslint-disable-next-line @silverhand/fp/no-mutation + elementRef.current.textContent = displayName; + } + if (!isInListView) { + const selectedSlugPart = getPathFn(metadata); + const targetSlug = + type === 'sdk' + ? slug.slice(0, Math.max(0, slug.indexOf(slugMiddlePart) + slugMiddlePart.length + 1)) + + selectedSlugPart + + slugLastPart + : slugFirstPart + selectedSlugPart + slug.slice(slug.indexOf(slugMiddlePart)); + // eslint-disable-next-line @silverhand/fp/no-mutating-methods + history.push(`/tutorial/${targetSlug}`); + } + }; + + return ( + <> + {titleParts.map((part) => { + if (part === sdkName) { + return ( + { + showDropdown('sdk'); + }} + onKeyDown={onKeyDownHandler(() => { + showDropdown('sdk'); + })} + > + {isInListView ? ( + defaultSdk ? ( + getSdkDisplayName(defaultSdk) + ) : ( + + Your SDK + + ) + ) : ( + part + )} + + ); + } + if (part === connectorName) { + return ( + { + showDropdown('connector'); + }} + onKeyDown={onKeyDownHandler(() => { + showDropdown('connector'); + })} + > + {isInListView ? ( + defaultConnector ? ( + getConnectorDisplayName(defaultConnector) + ) : ( + + Your provider + + ) + ) : ( + part + )} + + ); + } + return part; + })} + { + onSelectDropdown({ type: 'sdk', displayName, metadata: sdkMetadata }); + }} + onClose={() => { + setIsDropdownOpen(undefined); + }} + /> + { + onSelectDropdown({ type: 'connector', displayName, metadata: connectorMetadata }); + }} + onClose={() => { + setIsDropdownOpen(undefined); + }} + /> + + ); +}; + +export default TitleWithSelectionDropdown;