diff --git a/.prettierignore b/.prettierignore index 5e076fc800..bcc212213e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,7 @@ src/pages/**/*.mdx src/pages/**/*.md -examples/**/README.md \ No newline at end of file +examples/**/README.md + +# Fixture files should not be formatted by Prettier +data/onPostBuild/__fixtures__/*.mdx \ No newline at end of file diff --git a/data/createPages/index.ts b/data/createPages/index.ts index 4b96204cb0..430099d270 100644 --- a/data/createPages/index.ts +++ b/data/createPages/index.ts @@ -11,7 +11,7 @@ import { createContentMenuDataFromPage } from './createContentMenuDataFromPage'; import { DEFAULT_LANGUAGE } from './constants'; import { writeRedirectToConfigFile, getRedirectCount } from './writeRedirectToConfigFile'; import { siteMetadata } from '../../gatsby-config'; -import { GatsbyNode, Reporter } from 'gatsby'; +import { GatsbyNode } from 'gatsby'; import { examples, DEFAULT_EXAMPLE_LANGUAGES } from '../../src/data/examples/'; import { Example } from '../../src/data/examples/types'; @@ -252,7 +252,9 @@ export const createPages: GatsbyNode['createPages'] = async ({ // with nginx redirects writeRedirect(redirectFrom, pagePath); } else { - reporter.info(`[REDIRECTS] Skipping hash fragment redirect: ${redirectFrom} (hash: ${redirectFromUrl.hash})`); + reporter.info( + `[REDIRECTS] Skipping hash fragment redirect: ${redirectFrom} (hash: ${redirectFromUrl.hash})`, + ); } createRedirect({ @@ -276,7 +278,7 @@ export const createPages: GatsbyNode['createPages'] = async ({ contentOrderedList, contentMenu: contentMenuObject, script, - layout: { leftSidebar: true, rightSidebar: true, searchBar: true, template: 'base' }, + layout: { leftSidebar: true, rightSidebar: true, template: 'base' }, }, }); return slug; @@ -331,7 +333,7 @@ export const createPages: GatsbyNode['createPages'] = async ({ component: examplesTemplate, context: { example, - layout: { sidebar: false, searchBar: false, template: 'examples' }, + layout: { sidebar: false, template: 'examples' }, }, }); }; @@ -350,7 +352,9 @@ export const createPages: GatsbyNode['createPages'] = async ({ // with nginx redirects writeRedirect(redirectFrom, toPath); } else { - reporter.info(`[REDIRECTS] Skipping MDX hash fragment redirect: ${redirectFrom} (hash: ${redirectFromUrl.hash})`); + reporter.info( + `[REDIRECTS] Skipping MDX hash fragment redirect: ${redirectFrom} (hash: ${redirectFromUrl.hash})`, + ); } createRedirect({ diff --git a/data/onCreatePage.ts b/data/onCreatePage.ts index 09906c9a36..33a14e5371 100644 --- a/data/onCreatePage.ts +++ b/data/onCreatePage.ts @@ -5,7 +5,6 @@ import fs from 'fs'; export type LayoutOptions = { leftSidebar: boolean; rightSidebar: boolean; - searchBar: boolean; template: string; mdx: boolean; }; @@ -13,18 +12,17 @@ export type LayoutOptions = { const mdxWrapper = path.resolve('src/components/Layout/MDXWrapper.tsx'); const pageLayoutOptions: Record = { - '/docs': { leftSidebar: true, rightSidebar: false, searchBar: true, template: 'index', mdx: false }, + '/docs': { leftSidebar: true, rightSidebar: false, template: 'index', mdx: false }, '/docs/api/control-api': { leftSidebar: false, rightSidebar: false, - searchBar: true, template: 'control-api', mdx: false, }, - '/docs/sdks': { leftSidebar: false, rightSidebar: false, searchBar: true, template: 'sdk', mdx: false }, - '/examples': { leftSidebar: false, rightSidebar: false, searchBar: true, template: 'examples', mdx: false }, - '/docs/how-to/pub-sub': { leftSidebar: true, rightSidebar: true, searchBar: true, template: 'how-to', mdx: true }, - '/docs/404': { leftSidebar: false, rightSidebar: false, searchBar: false, template: '404', mdx: false }, + '/docs/sdks': { leftSidebar: false, rightSidebar: false, template: 'sdk', mdx: false }, + '/examples': { leftSidebar: false, rightSidebar: false, template: 'examples', mdx: false }, + '/docs/how-to/pub-sub': { leftSidebar: true, rightSidebar: true, template: 'how-to', mdx: true }, + '/docs/404': { leftSidebar: false, rightSidebar: false, template: '404', mdx: false }, }; // Function to extract code element classes from an MDX file @@ -66,9 +64,7 @@ export const onCreatePage: GatsbyNode['onCreatePage'] = async ({ page, actions } ...page, context: { ...page.context, - layout: pathOptions - ? pathOptions[1] - : { leftSidebar: true, rightSidebar: true, searchBar: true, template: 'base', mdx: isMDX }, + layout: pathOptions ? pathOptions[1] : { leftSidebar: true, rightSidebar: true, template: 'base', mdx: isMDX }, ...(isMDX ? { languages: Array.from(detectedLanguages) } : {}), }, component: isMDX ? `${mdxWrapper}?__contentFilePath=${page.component}` : page.component, diff --git a/data/onPostBuild/__fixtures__/input.mdx b/data/onPostBuild/__fixtures__/input.mdx index 46fda6209b..eda50c70de 100644 --- a/data/onPostBuild/__fixtures__/input.mdx +++ b/data/onPostBuild/__fixtures__/input.mdx @@ -1,5 +1,6 @@ --- title: Test Fixture +intro: "This is a test introduction" meta_description: "This is a test description" redirect_from: - /old-path @@ -67,4 +68,4 @@ Here's a code block with anchors and scripts that should be preserved: + \ No newline at end of file diff --git a/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap b/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap index 43dd0ac5bc..26969ecd3a 100644 --- a/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap +++ b/data/onPostBuild/__snapshots__/transpileMdxToMarkdown.test.ts.snap @@ -3,6 +3,8 @@ exports[`MDX to Markdown Transpilation Full transformation with fixture should transform comprehensive fixture correctly 1`] = ` "# Test Fixture +This is a test introduction + @@ -50,6 +52,5 @@ Here's a code block with anchors and scripts that should be preserved: -" +" `; diff --git a/data/onPostBuild/transpileMdxToMarkdown.test.ts b/data/onPostBuild/transpileMdxToMarkdown.test.ts index 3648d593be..7c6a77d59c 100644 --- a/data/onPostBuild/transpileMdxToMarkdown.test.ts +++ b/data/onPostBuild/transpileMdxToMarkdown.test.ts @@ -20,9 +20,10 @@ describe('MDX to Markdown Transpilation', () => { const inputPath = path.join(__dirname, '__fixtures__', 'input.mdx'); const input = fs.readFileSync(inputPath, 'utf-8'); - const { content, title } = transformMdxToMarkdown(input, siteUrl); + const { content, title, intro } = transformMdxToMarkdown(input, siteUrl); expect(title).toBe('Test Fixture'); + expect(intro).toBe('This is a test introduction'); expect(content).toMatchSnapshot(); }); @@ -37,6 +38,16 @@ Content without title`; transformMdxToMarkdown(input, siteUrl); }).toThrow('Missing title in frontmatter'); }); + + it('should not include intro or throw when it is not present', () => { + const input = `--- +title: Test Fixture +--- + +Content without intro`; + + expect(() => transformMdxToMarkdown(input, siteUrl)).not.toThrow(); + }); }); describe('removeImportExportStatements', () => { diff --git a/data/onPostBuild/transpileMdxToMarkdown.ts b/data/onPostBuild/transpileMdxToMarkdown.ts index 1f69a5d0ea..b456518d41 100644 --- a/data/onPostBuild/transpileMdxToMarkdown.ts +++ b/data/onPostBuild/transpileMdxToMarkdown.ts @@ -214,28 +214,30 @@ function convertImagePathsToGitHub(content: string): string { const githubBaseUrl = 'https://raw.githubusercontent.com/ably/docs/main/src'; const imageExtensions = '(?:png|jpg|jpeg|gif|svg|webp|bmp|ico)'; - return content - // Handle relative paths: ../../../images/...{ext} - .replace( - new RegExp(`!\\[([^\\]]*)\\]\\(((?:\\.\\.\\/)+)(images\\/[^)]+\\.${imageExtensions})\\)`, 'gi'), - (match, altText, relativePath, imagePath) => { - return `![${altText}](${githubBaseUrl}/${imagePath})`; - } - ) - // Handle absolute paths: /images/...{ext} - .replace( - new RegExp(`!\\[([^\\]]*)\\]\\(\\/(images\\/[^)]+\\.${imageExtensions})\\)`, 'gi'), - (match, altText, imagePath) => { - return `![${altText}](${githubBaseUrl}/${imagePath})`; - } - ) - // Handle direct paths: images/...{ext} (no prefix) - .replace( - new RegExp(`!\\[([^\\]]*)\\]\\((images\\/[^)]+\\.${imageExtensions})\\)`, 'gi'), - (match, altText, imagePath) => { - return `![${altText}](${githubBaseUrl}/${imagePath})`; - } - ); + return ( + content + // Handle relative paths: ../../../images/...{ext} + .replace( + new RegExp(`!\\[([^\\]]*)\\]\\(((?:\\.\\.\\/)+)(images\\/[^)]+\\.${imageExtensions})\\)`, 'gi'), + (match, altText, relativePath, imagePath) => { + return `![${altText}](${githubBaseUrl}/${imagePath})`; + }, + ) + // Handle absolute paths: /images/...{ext} + .replace( + new RegExp(`!\\[([^\\]]*)\\]\\(\\/(images\\/[^)]+\\.${imageExtensions})\\)`, 'gi'), + (match, altText, imagePath) => { + return `![${altText}](${githubBaseUrl}/${imagePath})`; + }, + ) + // Handle direct paths: images/...{ext} (no prefix) + .replace( + new RegExp(`!\\[([^\\]]*)\\]\\((images\\/[^)]+\\.${imageExtensions})\\)`, 'gi'), + (match, altText, imagePath) => { + return `![${altText}](${githubBaseUrl}/${imagePath})`; + }, + ) + ); } /** @@ -248,37 +250,32 @@ function convertRelativeUrls(content: string, siteUrl: string): string { // Match markdown links: [text](url) // Only convert URLs that start with / (relative) and are not external URLs or hash-only - return content.replace( - /\[([^\]]+)\]\(([^)]+)\)/g, - (match, linkText, url) => { - // Don't convert external URLs - if (url.startsWith('http://') || url.startsWith('https://')) { - return match; - } - - // Don't convert hash-only anchors - if (url.startsWith('#')) { - return match; - } - - // Convert relative URLs (starting with /) - if (url.startsWith('/')) { - return `[${linkText}](${baseUrl}${url})`; - } + return content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { + // Don't convert external URLs + if (url.startsWith('http://') || url.startsWith('https://')) { + return match; + } - // Keep other URLs as-is (relative paths without leading /) + // Don't convert hash-only anchors + if (url.startsWith('#')) { return match; } - ); + + // Convert relative URLs (starting with /) + if (url.startsWith('/')) { + return `[${linkText}](${baseUrl}${url})`; + } + + // Keep other URLs as-is (relative paths without leading /) + return match; + }); } /** * Replace template variables with readable placeholders */ function replaceTemplateVariables(content: string): string { - return content - .replace(/{{API_KEY}}/g, 'your-api-key') - .replace(/{{RANDOM_CHANNEL_NAME}}/g, 'your-channel-name'); + return content.replace(/{{API_KEY}}/g, 'your-api-key').replace(/{{RANDOM_CHANNEL_NAME}}/g, 'your-channel-name'); } /** @@ -309,7 +306,10 @@ function calculateOutputPath(relativeDirectory: string, fileName: string): strin /** * Transform MDX content to clean Markdown */ -function transformMdxToMarkdown(sourceContent: string, siteUrl: string): { content: string; title: string } { +function transformMdxToMarkdown( + sourceContent: string, + siteUrl: string, +): { content: string; title: string; intro?: string } { // Stage 1: Parse frontmatter const parsed = frontMatter(sourceContent); @@ -318,6 +318,7 @@ function transformMdxToMarkdown(sourceContent: string, siteUrl: string): { conte } const title = parsed.attributes.title; + const intro = parsed.attributes.intro; let content = parsed.body; // Stage 2: Remove import/export statements @@ -342,9 +343,9 @@ function transformMdxToMarkdown(sourceContent: string, siteUrl: string): { conte content = replaceTemplateVariables(content); // Stage 9: Prepend title as markdown heading - const finalContent = `# ${title}\n\n${content}`; + const finalContent = `# ${title}\n\n${intro ? `${intro}\n\n` : ''}${content}`; - return { content: finalContent, title }; + return { content: finalContent, title, intro }; } /** @@ -404,9 +405,7 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter const { data, errors } = await graphql(query); if (errors) { - reporter.panicOnBuild( - `${REPORTER_PREFIX} Error running GraphQL query: ${JSON.stringify(errors)}` - ); + reporter.panicOnBuild(`${REPORTER_PREFIX} Error running GraphQL query: ${JSON.stringify(errors)}`); return; } @@ -420,7 +419,7 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter if (!siteUrl) { reporter.panicOnBuild( - `${REPORTER_PREFIX} siteUrl is not configured in siteMetadata. Please check gatsby-config.ts` + `${REPORTER_PREFIX} siteUrl is not configured in siteMetadata. Please check gatsby-config.ts`, ); return; } @@ -442,22 +441,16 @@ export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ graphql, reporter successCount++; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - reporter.warn( - `${REPORTER_PREFIX} Failed to transpile ${node.internal.contentFilePath}: ${errorMessage}` - ); + reporter.warn(`${REPORTER_PREFIX} Failed to transpile ${node.internal.contentFilePath}: ${errorMessage}`); failureCount++; } } // Report summary if (failureCount > 0) { - reporter.warn( - `${REPORTER_PREFIX} Transpiled ${successCount} files, ${failureCount} failed` - ); + reporter.warn(`${REPORTER_PREFIX} Transpiled ${successCount} files, ${failureCount} failed`); } else { - reporter.info( - `${REPORTER_PREFIX} Successfully transpiled ${successCount} MDX files to Markdown` - ); + reporter.info(`${REPORTER_PREFIX} Successfully transpiled ${successCount} MDX files to Markdown`); } }; diff --git a/src/components/Layout/Header.test.tsx b/src/components/Layout/Header.test.tsx index b13224cb88..fb18203dfa 100644 --- a/src/components/Layout/Header.test.tsx +++ b/src/components/Layout/Header.test.tsx @@ -30,10 +30,6 @@ jest.mock('@ably/ui/core/LinkButton', () => { return MockButton; }); -jest.mock('../SearchBar', () => ({ - SearchBar: jest.fn(() =>
SearchBar
), -})); - jest.mock('./LeftSidebar', () => ({ __esModule: true, default: jest.fn(() =>
LeftSidebar
), diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index aa8cfa28d7..f727b40413 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useLocation } from '@reach/router'; import { graphql, useStaticQuery } from 'gatsby'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; @@ -127,6 +127,30 @@ const Header: React.FC = () => { setIsMobileMenuOpen(false); }, [activePage]); + const handleLogout = useCallback(async () => { + if (sessionState.logOut.href && sessionState.logOut.token) { + try { + await fetch(sessionState.logOut.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + _method: 'delete', + authenticity_token: sessionState.logOut.token, + }), + }); + + track('docs_logout_button_clicked'); + + // Reload the current page after successful logout + window.location.reload(); + } catch (error) { + console.error('Logout failed:', error); + } + } + }, [sessionState.logOut]); + return (
@@ -263,7 +287,7 @@ const Header: React.FC = () => { )} - diff --git a/src/components/Layout/LanguageSelector.tsx b/src/components/Layout/LanguageSelector.tsx index cd2ef52474..00646c0471 100644 --- a/src/components/Layout/LanguageSelector.tsx +++ b/src/components/Layout/LanguageSelector.tsx @@ -67,7 +67,10 @@ export const LanguageSelector = () => { const selectedLang = languageInfo[selectedOption.label]; return ( -
+
{ if (activeTriggerRef.current) { setTimeout(() => { - activeTriggerRef.current?.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); + const element = activeTriggerRef.current; + const scrollableContainer = element?.closest('.overflow-y-auto'); + + if (element && scrollableContainer) { + const elementRect = element.getBoundingClientRect(); + const containerRect = scrollableContainer.getBoundingClientRect(); + const scrollOffset = elementRect.top - containerRect.top - containerRect.height / 2 + elementRect.height / 2; + + scrollableContainer.scrollBy({ + top: scrollOffset, + behavior: 'smooth', + }); + } }, 200); } }, [activePage.tree]); @@ -107,13 +116,24 @@ const ChildAccordion = ({ content, tree }: { content: (NavProductPage | NavProdu {page.name} + {page.external && ( + + )} ) )} diff --git a/src/components/Layout/MDXWrapper.test.tsx b/src/components/Layout/MDXWrapper.test.tsx index 9111791141..edb268741d 100644 --- a/src/components/Layout/MDXWrapper.test.tsx +++ b/src/components/Layout/MDXWrapper.test.tsx @@ -255,7 +255,7 @@ describe('MDXWrapper structured data', () => { meta_description: 'Test description', }, languages: [], - layout: { mdx: true, leftSidebar: true, rightSidebar: true, searchBar: true, template: 'docs' }, + layout: { mdx: true, leftSidebar: true, rightSidebar: true, template: 'docs' }, }; const defaultLocation = { diff --git a/src/components/Layout/mdx/PageHeader.tsx b/src/components/Layout/mdx/PageHeader.tsx index 850ac39fdf..e07fffe386 100644 --- a/src/components/Layout/mdx/PageHeader.tsx +++ b/src/components/Layout/mdx/PageHeader.tsx @@ -1,6 +1,7 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useLocation } from '@reach/router'; import * as Tooltip from '@radix-ui/react-tooltip'; +import cn from '@ably/ui/core/utils/cn'; import Icon from '@ably/ui/core/Icon'; import { IconName } from '@ably/ui/core/Icon/types'; import { LanguageSelector } from '../LanguageSelector'; @@ -20,6 +21,8 @@ export const PageHeader: React.FC = ({ title, intro }) => { const { activePage } = useLayoutContext(); const { language, product, page } = activePage; const location = useLocation(); + const [copyTooltipOpen, setCopyTooltipOpen] = useState(false); + const [copyTooltipContent, setCopyTooltipContent] = useState('Copy'); const llmLinks = useMemo(() => { const prompt = `Tell me more about ${product ? productData[product]?.nav.name : 'Ably'}'s '${page.name}' feature from https://ably.com${page.link}${language ? ` for ${languageInfo[language]?.label}` : ''}`; @@ -41,23 +44,94 @@ export const PageHeader: React.FC = ({ title, intro }) => { [activePage.languages, product], ); + const handleCopyMarkdown = useCallback(async () => { + try { + const response = await fetch(`${location.pathname}.md`); + + if (!response.ok) { + throw new Error(`Failed to fetch markdown: ${response.status} ${response.statusText}`); + } + + const contentType = response.headers.get('Content-Type'); + if (!contentType || !contentType.includes('text/markdown')) { + throw new Error(`Invalid content type: expected text/markdown, got ${contentType}`); + } + + const content = await response.text(); + await navigator.clipboard.writeText(content); + setCopyTooltipContent('Copied!'); + setCopyTooltipOpen(true); + setTimeout(() => { + setCopyTooltipOpen(false); + setTimeout(() => setCopyTooltipContent('Copy'), 150); + }, 2000); + + track('markdown_copy_link_clicked', { + location: location.pathname, + }); + } catch (error) { + console.error('Failed to copy markdown:', error); + setCopyTooltipContent('Error!'); + setCopyTooltipOpen(true); + setTimeout(() => { + setCopyTooltipOpen(false); + setTimeout(() => setCopyTooltipContent('Copy'), 150); + }, 2000); + } + }, [location.pathname]); + return (
-

{title}

-

- {intro} -

+

{title}

+ {intro && ( +

+ {intro} +

+ )} -
+
{showLanguageSelector && ( -
+
)} -
- Open in - + +
+ Markdown + + + + + + {copyTooltipContent} + + + + + { + track('markdown_preview_link_clicked', { + location: location.pathname, + }); + }} + > + + + + + View + + +
+
+ Open in {llmLinks.map(({ model, label, icon, link }) => ( @@ -83,8 +157,8 @@ export const PageHeader: React.FC = ({ title, intro }) => { ))} - -
+
+
); diff --git a/src/components/Layout/utils/heights.ts b/src/components/Layout/utils/heights.ts index 408a202358..b58a0f81e0 100644 --- a/src/components/Layout/utils/heights.ts +++ b/src/components/Layout/utils/heights.ts @@ -4,5 +4,5 @@ to Layout components, consider these values and update where necessary. */ -export const LANGUAGE_SELECTOR_HEIGHT = 38; +export const LANGUAGE_SELECTOR_HEIGHT = 32; export const INKEEP_ASK_BUTTON_HEIGHT = 48; diff --git a/src/components/SearchBar/EmptyState.tsx b/src/components/SearchBar/EmptyState.tsx deleted file mode 100644 index 05793d8d0d..0000000000 --- a/src/components/SearchBar/EmptyState.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React, { HTMLAttributes } from 'react'; -import cn from '@ably/ui/core/utils/cn'; - -export const EmptyState = ({ className, ...props }: HTMLAttributes) => { - return
; -}; diff --git a/src/components/SearchBar/KeyIcon.module.css b/src/components/SearchBar/KeyIcon.module.css deleted file mode 100644 index 3b45381528..0000000000 --- a/src/components/SearchBar/KeyIcon.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.keyIcon { - @apply text-dark-grey bg-white hidden md:flex items-center justify-center rounded shadow w-6 h-6 font-medium; - min-width: 24px; -} diff --git a/src/components/SearchBar/KeyIcon.tsx b/src/components/SearchBar/KeyIcon.tsx deleted file mode 100644 index c38a2746d4..0000000000 --- a/src/components/SearchBar/KeyIcon.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React, { HTMLAttributes } from 'react'; -import cn from '@ably/ui/core/utils/cn'; - -import { keyIcon } from './KeyIcon.module.css'; - -export const KeyIcon = ({ className = '', ...props }: HTMLAttributes) => { - return
; -}; diff --git a/src/components/SearchBar/SearchBar.module.css b/src/components/SearchBar/SearchBar.module.css deleted file mode 100644 index bffd1b0f06..0000000000 --- a/src/components/SearchBar/SearchBar.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.searchInput { - @apply font-medium block outline-none w-full min-w-[11rem] h-[2.375rem] pl-[3.125rem] md:max-w-[25rem] bg-neutral-100 border border-neutral-300 rounded-lg hover:border-light-grey focus-within:bg-white focus-within:shadow-input focus-within:border-transparent focus-within:outline-gui-focus; -} - -.searchInputNoPadding { - padding-left: 0; -} diff --git a/src/components/SearchBar/SearchBar.test.tsx b/src/components/SearchBar/SearchBar.test.tsx deleted file mode 100644 index ddcb078f84..0000000000 --- a/src/components/SearchBar/SearchBar.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { useStaticQuery } from 'gatsby'; -import { render } from '@testing-library/react'; -import { DisplayMode, SearchBar } from '.'; - -describe('', () => { - const externalScriptsData = { - inkeepChatEnabled: false, - inkeepSearchEnabled: false, - }; - - beforeEach(() => { - useStaticQuery.mockReturnValue({ - site: { - siteMetadata: { - externalScriptsData, - }, - }, - }); - }); - - it('should not render search when Inkeep is disabled', () => { - const { container } = render(); - expect(container.querySelector('#inkeep-search')).not.toBeInTheDocument(); - }); - - describe('when Inkeep search is enabled', () => { - beforeEach(() => { - externalScriptsData.inkeepSearchEnabled = true; - }); - - it('should render the Inkeep search component', () => { - render(); - expect(document.querySelector('#inkeep-search')).toBeInTheDocument(); - }); - }); -}); diff --git a/src/components/SearchBar/SearchBar.tsx b/src/components/SearchBar/SearchBar.tsx deleted file mode 100644 index bee8dde594..0000000000 --- a/src/components/SearchBar/SearchBar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { graphql, useStaticQuery } from 'gatsby'; -import cn from '@ably/ui/core/utils/cn'; - -import { InkeepSearchBar } from './InkeepSearchBar'; - -import { searchInput, searchInputNoPadding } from './SearchBar.module.css'; - -export enum DisplayMode { - FULL_SCREEN = 'FULL_SCREEN', - MOBILE = 'MOBILE', -} - -export const SearchBar = ({ - displayMode, - displayLocation, - extraStyleOptions, -}: { - displayMode?: DisplayMode; - displayLocation?: string; - extraStyleOptions?: object; -}) => { - const extraWrapperContainerStyle = extraStyleOptions && extraStyleOptions.wrapperContainer; - const extraInputStyle = extraStyleOptions && extraStyleOptions.inputContainer; - - const { - site: { - siteMetadata: { externalScriptsData }, - }, - } = useStaticQuery(graphql` - query { - site { - siteMetadata { - externalScriptsData { - inkeepChatEnabled - inkeepSearchEnabled - } - } - } - } - `); - - return ( -
-
- {externalScriptsData.inkeepSearchEnabled && ( - - )} -
-
- ); -}; diff --git a/src/components/SearchBar/SuggestionBox.module.css b/src/components/SearchBar/SuggestionBox.module.css deleted file mode 100644 index e71477d0dc..0000000000 --- a/src/components/SearchBar/SuggestionBox.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.container { - @apply rounded-lg bg-white absolute left-0 overflow-y-auto border border-mid-grey shadow-container-subtle p-4 max-w-[100%] md:max-w-screen-lg z-20 overflow-x-hidden; - width: 800px; - top: 60px; - max-height: 578px; -} - -.hitItem:hover .titleStyle, -.hitItem:focus .titleStyle { - @apply text-gui-active; -} diff --git a/src/components/SearchBar/SuggestionBox.tsx b/src/components/SearchBar/SuggestionBox.tsx deleted file mode 100644 index 1f79e15674..0000000000 --- a/src/components/SearchBar/SuggestionBox.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import htmr from 'htmr'; -import cn from '@ably/ui/core/utils/cn'; -import clamp from 'lodash/clamp'; -import Icon from '@ably/ui/core/Icon'; - -import { HitType, useKeyPress } from 'src/hooks'; - -import { EmptyState } from './EmptyState'; -import { container, hitItem, titleStyle } from './SuggestionBox.module.css'; - -type Props = { - results: HitType[] | null; - isActive: boolean; - error?: { message: string } | null; - query: string; - displayLocation: string; -}; - -export const SuggestionBox = ({ results, isActive, error, query, displayLocation }: Props) => { - const totalResults = results?.length ?? 0; - const [selectedItem, setSelectedItem] = useState(null); - - const handleSelectHit = (index: number) => { - setSelectedItem(index); - document.getElementById(`suggestion-${index}`)?.focus(); - }; - - const handleResultItemSelect = ({ key }: KeyboardEvent) => { - const index = - key === 'ArrowDown' - ? clamp(selectedItem !== null ? selectedItem + 1 : 0, 0, totalResults) - : clamp(selectedItem !== null ? selectedItem - 1 : totalResults - 1, 0, totalResults); - - handleSelectHit(index); - }; - - useKeyPress(['ArrowDown', 'ArrowUp'], handleResultItemSelect); - - if (!isActive || !query) { - return null; - } - - const containerStyle = displayLocation === 'homepage' ? { maxWidth: '100%' } : null; - - return ( -
-
Results from docs
- {results && totalResults > 0 ? ( - results.map((hit, index) => { - const { title, highlight, meta_description, url, id } = hit; - const [pageTitle] = title.split(' / '); - const body = meta_description ?? highlight; - - return ( - -

{pageTitle}

-
{htmr(body)}
-
- ); - }) - ) : error ? ( - - We couldn't perform the search due to an API error. Please try again or{' '} - - raise a support ticket - {' '} - if the problem persists. - - ) : ( - - We couldn't find an exact match for ‘{query}’ in the docs, but you - may be able to find the results you're looking for on{' '} - - our main site - - - . - - )} -
- ); -}; diff --git a/src/components/SearchBar/index.ts b/src/components/SearchBar/index.ts deleted file mode 100644 index 3d7e42eb56..0000000000 --- a/src/components/SearchBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SearchBar'; diff --git a/src/external-scripts/inkeep.ts b/src/external-scripts/inkeep.ts index 417834d4fa..830d188f2a 100644 --- a/src/external-scripts/inkeep.ts +++ b/src/external-scripts/inkeep.ts @@ -219,28 +219,40 @@ export const inkeepOnLoad = ( shouldShowAskAICard: false, }; + // Only enable keyboard shortcut for one instance to prevent multiple popups if (inkeepSearchEnabled) { - loadInkeepSearch(config, 'inkeep-search'); + loadInkeepSearch(config, 'inkeep-search', false); // Keep keyboard shortcut } if (inkeepChatEnabled) { - loadInkeepSearch(config, 'inkeep-ai-chat'); + loadInkeepSearch(config, 'inkeep-ai-chat', true); // Disable keyboard shortcut } }; -const loadInkeepSearch = (config: object, elementId: string) => { +const loadInkeepSearch = (config: object, elementId: string, disableShortcut: boolean) => { const searchBar = document.getElementById(elementId); if (!searchBar) { return; } + // Check if already initialized to prevent duplicate instances + if (searchBar.hasChildNodes()) { + return; + } + const defaultView = elementId === 'inkeep-ai-chat' ? 'chat' : 'search'; - window.Inkeep.SearchBar(`#${searchBar.id}`, { + const widgetConfig: Record = { ...config, defaultView, shouldShowAskAICard: false, - }).remount(); + }; + + if (disableShortcut) { + widgetConfig.modalSettings = { shortcutKey: '' }; + } + + window.Inkeep.SearchBar(`#${searchBar.id}`, widgetConfig); }; export type InkeepUser = {