Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 122 additions & 34 deletions components/AlgoliaSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
/* eslint-disable no-underscore-dangle */
import type { DocSearchHit, InternalDocSearchHit, StoredDocSearchHit } from '@docsearch/react';
import { DocSearchModal } from '@docsearch/react';
import type {
DocSearchHit,
InternalDocSearchHit,
StoredDocSearchHit,
} from '@docsearch/react';
import clsx from 'clsx';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { createPortal } from 'react-dom';

const DocSearchModal = dynamic(
() =>
import('@docsearch/react').then((mod) => ({ default: mod.DocSearchModal })),
{ ssr: false },
);
Comment on lines +2 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run the configured formatter before merge.

prettier/prettier reports errors in this import and dynamic-import block, with the same issue in many later changed ranges. Format components/AlgoliaSearch.tsx with the repository configuration so lint can pass.

🧰 Tools
🪛 ESLint

[error] 2-6: Replace ⏎··DocSearchHit,⏎··InternalDocSearchHit,⏎··StoredDocSearchHit,⏎ with ·DocSearchHit,·InternalDocSearchHit,·StoredDocSearchHit·

(prettier/prettier)


[error] 12-19: Replace ⏎··createContext,⏎··useCallback,⏎··useContext,⏎··useEffect,⏎··useRef,⏎··useState,⏎ with ·createContext,·useCallback,·useContext,·useEffect,·useRef,·useState·

(prettier/prettier)


[error] 22-24: Replace ⏎··()·=>⏎····import('@docsearch/react').then((mod)·=>·({·default:·mod.DocSearchModal·})), with ()·=>·import('@docsearch/react').then((mod)·=>·({·default:·mod.DocSearchModal·})),·{

(prettier/prettier)


[error] 25-25: Replace ·{·ssr:·false·}, with ·ssr:·false

(prettier/prettier)


[error] 26-26: Insert }

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/AlgoliaSearch.tsx` around lines 2 - 26, Run the
repository-configured Prettier formatter on components/AlgoliaSearch.tsx,
including the import and DocSearchModal dynamic-import block and all other
changed ranges, without altering behavior.

Source: Linters/SAST tools


export const INDEX_NAME = 'asyncapi';
export const DOCS_INDEX_NAME = 'asyncapi-docs';
const APP_ID = 'Z621OGRI9Y';
Expand Down Expand Up @@ -46,8 +63,17 @@
onInput?: (e: React.KeyboardEvent) => void;
}

type ISearchButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> & {
children?: React.ReactNode | (({ actionKey }: { actionKey: { shortKey: string; key: string } }) => React.ReactNode);
type ISearchButtonProps = Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
'children'
> & {
children?:
| React.ReactNode
| (({
actionKey,
}: {
actionKey: { shortKey: string; key: string };
}) => React.ReactNode);
indexName?: string;
};

Expand All @@ -64,7 +90,8 @@

a.href = item.url;

const hash = a.hash === '#content-wrapper' || a.hash === '#header' ? '' : a.hash;
const hash =
a.hash === '#content-wrapper' || a.hash === '#header' ? '' : a.hash;

if (item.hierarchy?.lvl0) {
// eslint-disable-next-line no-param-reassign
Expand All @@ -75,10 +102,15 @@
...item,
url: `${a.pathname}${hash}`,
__is_result: () => true,
__is_parent: () => item.type === 'lvl1' && items.length > 1 && index === 0,
__is_child: () => item.type !== 'lvl1' && items.length > 1 && items[0].type === 'lvl1' && index !== 0,
__is_parent: () =>
item.type === 'lvl1' && items.length > 1 && index === 0,
__is_child: () =>
item.type !== 'lvl1' &&
items.length > 1 &&
items[0].type === 'lvl1' &&
index !== 0,
__is_first: () => index === 1,
__is_last: () => index === items.length - 1 && index !== 0
__is_last: () => index === items.length - 1 && index !== 0,
};
});
}
Expand All @@ -97,7 +129,7 @@
'DocSearch-Hit--Parent': hit.__is_parent?.(),
'DocSearch-Hit--FirstChild': hit.__is_first?.(),
'DocSearch-Hit--LastChild': hit.__is_last?.(),
'DocSearch-Hit--Child': hit.__is_child?.()
'DocSearch-Hit--Child': hit.__is_child?.(),
})}
>
{children}
Expand All @@ -114,14 +146,18 @@
const router = useRouter();

return createPortal(
<div className='dark:text-dark-text'>
<div className="dark:text-dark-text">
<DocSearchModal
initialQuery={initialQuery}
initialScrollY={window.scrollY}
searchParameters={{
distinct: 1
distinct: 1,
}}
placeholder={indexName === DOCS_INDEX_NAME ? 'Search documentation' : 'Search resources'}
placeholder={
indexName === DOCS_INDEX_NAME
? 'Search documentation'
: 'Search resources'
}
onClose={onClose}
indexName={indexName}
apiKey={API_KEY}
Expand All @@ -130,7 +166,7 @@
navigate({ itemUrl }) {
onClose();
router.push(itemUrl);
}
},
}}
hitComponent={Hit}
transformItems={transformItems}
Expand All @@ -139,7 +175,7 @@
}}
/>
</div>,
document.body
document.body,
);
}

Expand All @@ -153,7 +189,10 @@
const { tagName } = element as HTMLElement;

return (
(element as HTMLElement).isContentEditable || tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'
(element as HTMLElement).isContentEditable ||
tagName === 'INPUT' ||
tagName === 'SELECT' ||
tagName === 'TEXTAREA'
);
}

Expand All @@ -163,22 +202,24 @@
*/
function getActionKey() {
if (typeof navigator !== 'undefined') {
if (/(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent || navigator.platform)) {
if (
/(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent || navigator.platform)
) {
return {
shortKey: '⌘',
key: 'Command'
key: 'Command',
};
}

return {
shortKey: 'Ctrl',
key: 'Control'
key: 'Control',
};
}

return {
shortKey: 'Ctrl',
key: 'Control'
key: 'Control',
};
}

Expand All @@ -187,7 +228,11 @@
* @description The hook used for the Algolia search keyboard events
* @param {IUseDocSearchKeyboardEvents} props - The props of the useDocSearchKeyboardEvents hook
*/
function useDocSearchKeyboardEvents({ isOpen, onOpen, onClose }: IUseDocSearchKeyboardEvents) {
function useDocSearchKeyboardEvents({
isOpen,
onOpen,
onClose,
}: IUseDocSearchKeyboardEvents) {
useEffect(() => {
/**
* @description The function used to handle the keyboard event.
Expand All @@ -213,7 +258,9 @@
if (typeof document !== 'undefined') {
const loc = document.location;

indexName = loc.pathname.startsWith('/docs') ? DOCS_INDEX_NAME : INDEX_NAME;
indexName = loc.pathname.startsWith('/docs')
? DOCS_INDEX_NAME
: INDEX_NAME;
}
onOpen(indexName);
}
Expand All @@ -233,7 +280,11 @@
* @description The Algolia search component used for searching the website
* @param {React.ReactNode} children - The content of the page
*/
export default function AlgoliaSearch({ children }: { children: React.ReactNode }) {
export default function AlgoliaSearch({
children,
}: {
children: React.ReactNode;
}) {

Check warning on line 287 in components/AlgoliaSearch.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-2tu0BX1qv0iLRvOfH&open=AZ-2tu0BX1qv0iLRvOfH&pullRequest=5673
const [isOpen, setIsOpen] = useState(false);
const [indexName, setIndexName] = useState<string>(INDEX_NAME);
const [initialQuery, setInitialQuery] = useState<string>();
Expand All @@ -245,7 +296,7 @@
}
setIsOpen(true);
},
[setIsOpen, setIndexName]
[setIsOpen, setIndexName],
);

const onClose = useCallback(() => {
Expand All @@ -257,23 +308,51 @@
setIsOpen(true);
setInitialQuery(e.key);
},
[setIsOpen, setInitialQuery]
[setIsOpen, setInitialQuery],
);

useDocSearchKeyboardEvents({
isOpen,
onOpen,
onClose,
onInput
onInput,
});

useEffect(() => {
if (isOpen) {
// Load Algolia CSS on demand when modal first opens
const linkId = 'docsearch-css';

if (!document.getElementById(linkId)) {
const link = document.createElement('link');

link.id = linkId;
link.rel = 'stylesheet';
link.href = 'https://cdn.jsdelivr.net/npm/@docsearch/css@3';
document.head.appendChild(link);
}
}
}, [isOpen]);

return (
<>
<Head>
<link rel='preconnect' href={`https://${APP_ID}-dsn.algolia.net`} crossOrigin='anonymous' />
<link
rel="preconnect"
href={`https://${APP_ID}-dsn.algolia.net`}
crossOrigin="anonymous"
/>
</Head>
<SearchContext.Provider value={{ isOpen, onOpen, onClose, onInput }}>{children}</SearchContext.Provider>
{isOpen && <AlgoliaModal initialQuery={initialQuery ?? ''} onClose={onClose} indexName={indexName} />}
<SearchContext.Provider value={{ isOpen, onOpen, onClose, onInput }}>

Check warning on line 346 in components/AlgoliaSearch.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

The object passed as the value prop to the Context provider changes every render. To fix this consider wrapping it in a useMemo hook.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-2tu0BX1qv0iLRvOfI&open=AZ-2tu0BX1qv0iLRvOfI&pullRequest=5673
{children}
</SearchContext.Provider>
{isOpen && (
<AlgoliaModal
initialQuery={initialQuery ?? ''}
onClose={onClose}
indexName={indexName}
/>
)}
Comment on lines +321 to +355

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wait for the DocSearch stylesheet before mounting the modal.

When isOpen becomes true, React mounts AlgoliaModal before the effect appends and loads the stylesheet. The first open can show an unstyled modal. A failed request also prevents retries because the existing element ID is treated as success. Track stylesheet load and error states, and render the modal only after the stylesheet is ready.

🧰 Tools
🪛 ESLint

[error] 340-344: Replace ⏎··········rel="preconnect"⏎··········href={https://${APP_ID}-dsn.algolia.net}⏎··········crossOrigin="anonymous"⏎······· with ·rel='preconnect'·href={https://${APP_ID}-dsn.algolia.net}·crossOrigin='anonymous'

(prettier/prettier)


[error] 346-348: Replace ⏎········{children}⏎······ with {children}

(prettier/prettier)


[error] 349-355: Replace (⏎········<AlgoliaModal⏎··········initialQuery={initialQuery·??·''}⏎··········onClose={onClose}⏎··········indexName={indexName}⏎········/>⏎······) with <AlgoliaModal·initialQuery={initialQuery·??·''}·onClose={onClose}·indexName={indexName}·/>

(prettier/prettier)

🪛 GitHub Check: SonarCloud Code Analysis

[warning] 346-346: The object passed as the value prop to the Context provider changes every render. To fix this consider wrapping it in a useMemo hook.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-2tu0BX1qv0iLRvOfI&open=AZ-2tu0BX1qv0iLRvOfI&pullRequest=5673

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/AlgoliaSearch.tsx` around lines 321 - 355, Update the
stylesheet-loading useEffect and the isOpen rendering flow in the Algolia search
component to track DocSearch link load and error states, resetting readiness
when opening and allowing retries after failures. Render AlgoliaModal only once
the stylesheet has fired load successfully, while preserving the existing modal
props and close behavior.

</>
);
}
Expand All @@ -283,7 +362,11 @@
* @description The search button component used for opening the Algolia search
* @param {ISearchButtonProps} props - The props of the search button
*/
export function SearchButton({ children, indexName = INDEX_NAME, ...props }: ISearchButtonProps) {
export function SearchButton({
children,
indexName = INDEX_NAME,
...props
}: ISearchButtonProps) {
const { onOpen, onInput } = useContext(SearchContext);
const searchButtonRef = useRef<HTMLButtonElement>(null);
const actionKey = getActionKey();
Expand All @@ -296,7 +379,11 @@
* @returns {void}
*/
function onKeyDown(event: KeyboardEvent) {
if (searchButtonRef && searchButtonRef.current === document.activeElement && onInput) {
if (
searchButtonRef &&
searchButtonRef.current === document.activeElement &&

Check warning on line 384 in components/AlgoliaSearch.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ-2tu0BX1qv0iLRvOfJ&open=AZ-2tu0BX1qv0iLRvOfJ&pullRequest=5673
onInput
) {
if (/[a-zA-Z0-9]/.test(event.key)) {
onInput(event as unknown as React.KeyboardEvent);
}
Expand All @@ -310,17 +397,18 @@
};
}, [onInput, searchButtonRef]);

const childContent = typeof children === 'function' ? children({ actionKey }) : children;
const childContent =
typeof children === 'function' ? children({ actionKey }) : children;

return (
<button
type='button'
type="button"
ref={searchButtonRef}
onClick={() => {
onOpen(indexName);
}}
{...props}
data-testid='Search-Button'
data-testid="Search-Button"
>
{childContent}
</button>
Expand Down
Loading
Loading