diff --git a/components/DocumentationNavigation/DocsNavigationList.tsx b/components/DocumentationNavigation/DocsNavigationList.tsx index efd8995fb..520c3de4d 100644 --- a/components/DocumentationNavigation/DocsNavigationList.tsx +++ b/components/DocumentationNavigation/DocsNavigationList.tsx @@ -248,7 +248,7 @@ export const DocsNavigationList = ({ navItems }: DocsNavProps) => { return ( <> - {navItems.map((categoryData) => ( + {navItems?.map((categoryData) => ( { + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [isSortOpen, setIsSortOpen] = useState(false); + + const filterOptions = ['FilterOp1', 'FilterOp2', 'FilterOp3']; + const sortOptions = ['Relevance', 'Newest First', 'Oldest First']; + + const toggleFilterDropdown = () => { + setIsFilterOpen(!isFilterOpen); + setIsSortOpen(false); + }; + + const toggleSortDropdown = () => { + setIsSortOpen(!isSortOpen); + setIsFilterOpen(false); + }; + + return ( +
+
+ Results for "{query}" +
+
+ {/* Filter Button */} +
+ {/* TODO: Implement Feature and Sort buttons - https://github.com/tinacms/tina.io/issues/2550 */} + {/* */} + {isFilterOpen && ( +
+ {filterOptions.map((option) => ( +
{ + + setIsFilterOpen(false); + }} + > + {option} +
+ ))} +
+ )} +
+ + {/* Sort Button */} + {/*
+ + {isSortOpen && ( +
+ {sortOptions.map((option) => ( +
{ + + setIsSortOpen(false); + }} + > + {option} +
+ ))} +
+ )} +
*/} +
+
+ ); +}; + +export const SearchTabs = ({ query }: { query: string }) => { + const [activeTab, setActiveTab] = useState('DOCS'); + const [algoliaSearchResults, setAlgoliaSearchResults] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchResults = async () => { + setIsLoading(true); + setAlgoliaSearchResults(null); + if (query) { + const results = await fetchAlgoliaSearchResults(query); + setAlgoliaSearchResults(results); + } + setIsLoading(false); + }; + + fetchResults(); + }, [query]); + + const tabRefs = useRef<(HTMLButtonElement | null)[]>([]); + const activeTabIndex = activeTab === 'DOCS' ? 0 : 1; + const activeTabElement = tabRefs.current[activeTabIndex]; + const left = activeTabElement?.offsetLeft || 0; + const width = (activeTabElement?.offsetWidth || 0) + 30; + + const numberOfResults = + (algoliaSearchResults?.docs?.count + algoliaSearchResults?.blogs?.count) || 0; + + return ( +
+
+
+ {/* Navigation Buttons */} + + + {/* Search Results Count */} +
+ {numberOfResults}{' '} + Results +
+
+ {isLoading && ( +
+ Mustering all the Llamas... +
+ )} + + {(numberOfResults == 0 && isLoading==false) &&
No Results Found...
} +
+
+ ); +}; + +export const SearchBody = ({ + results, + activeItem, +}: { + results: any; + activeItem: string; +}) => { + const bodyItem = activeItem === 'DOCS' ? results?.docs : results?.blogs; + return ( +
+ {bodyItem?.results.map((item: any) => ( +
+ +

+ {item.title} +

+

+ {item.excerpt} +

+ +
+ ))} +
+ ); +}; \ No newline at end of file diff --git a/components/docsSearch/SearchNavigation.tsx b/components/docsSearch/SearchNavigation.tsx new file mode 100644 index 000000000..52fa68d82 --- /dev/null +++ b/components/docsSearch/SearchNavigation.tsx @@ -0,0 +1,235 @@ +import { DocsNavigationList } from 'components/DocumentationNavigation/DocsNavigationList'; +import { VersionSelect } from 'components/DocumentationNavigation/VersionSelect'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useState, useEffect, useRef } from 'react'; +import { HiMagnifyingGlass } from 'react-icons/hi2'; +import { fetchAlgoliaSearchResults } from 'utils/new-search'; + +export const SearchResultsOverflowBody = ({ + results, + activeItem, + query, + numberOfResults, + isLoading, +}: { + results: any; + activeItem: string; + query: string; + numberOfResults: number; + isLoading: boolean; +}) => { + const bodyItem = activeItem === 'DOCS' ? results?.docs : results?.blogs; + return ( +
+ {bodyItem?.results.slice(0, 10).map((item: any) => ( +
+ +

+ {item.title} +

+

+ {item.excerpt} +

+ +
+ ))} +
+ {numberOfResults > 0 ? ( + +
+ See All {numberOfResults} Results +
+ + ) : ( + !isLoading && ( +
+ No Llamas Found... +
+ ) + )} +
+
+ ); +}; + +export const SearchResultsOverflowTabs = ({ query }) => { + const [activeTab, setActiveTab] = useState('DOCS'); + const [algoliaSearchResults, setAlgoliaSearchResults] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchResults = async () => { + setIsLoading(true); + setAlgoliaSearchResults(null); + if (query) { + const results = await fetchAlgoliaSearchResults(query); + setAlgoliaSearchResults(results); + } + setIsLoading(false); + }; + + fetchResults(); + }, [query]); + + const tabRefs = useRef<(HTMLButtonElement | null)[]>([]); + const activeTabIndex = activeTab === 'DOCS' ? 0 : 1; + const activeTabElement = tabRefs.current[activeTabIndex]; + const left = activeTabElement?.offsetLeft || 0; + const width = (activeTabElement?.offsetWidth || 0) + 30; + const numberOfResults = + algoliaSearchResults?.docs?.count + algoliaSearchResults?.blogs?.count; + + return ( +
+
+
+ {/* Navigation Buttons */} + +
+ {isLoading && ( +
+ Mustering all the Llamas... +
+ )} +
+ +
+
+
+ ); +}; + +export const SearchResultsOverflow = ({ query }) => { + return ( +
+ +
+ ); +}; + +export const LeftHandSideHeader = ({}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [userHasTyped, setUserHasTyped] = useState(false); + const [searchOverFlowOpen, setSearchOverflowOpen] = useState(false); + const router = useRouter(); + + const handleKeyChange = (e: React.ChangeEvent) => { + setSearchOverflowOpen(true); + const value = e.target.value; + setSearchTerm(value); + + if (value.trim()) { + setUserHasTyped(true); + fetchSearchResults(value); + } else { + setUserHasTyped(false); + setSearchResults(null); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchTerm.trim()) { + router.push(`/search?query=${encodeURIComponent(searchTerm)}`); + setSearchOverflowOpen(false); + } + }; + + const fetchSearchResults = async (query: string) => { + setIsLoading(true); + try { + const results = await fetchAlgoliaSearchResults(query); + setSearchResults(results); + } catch (error) { + console.error('Error fetching search results:', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

+ Tina Docs +

+
+ +
+
+
+ setSearchOverflowOpen(true)} + /> + { + if (searchTerm.trim()) { + router.push(`/search?query=${encodeURIComponent(searchTerm)}`); + setSearchOverflowOpen(false); + } + }} + />{' '} +
+ {userHasTyped && searchOverFlowOpen && ( + + )} +
+ ); +}; + +export const LeftHandSideParentContainer = ({ tableOfContents }) => { + return ( +
+ +
+ +
+
+ ); +}; diff --git a/components/search/input.tsx b/components/search/input.tsx index ab9a2983e..d0712ee5e 100644 --- a/components/search/input.tsx +++ b/components/search/input.tsx @@ -6,6 +6,7 @@ import * as debounce from 'lodash/debounce' import { IconWrapper, Input, SearchContainer } from './styles' import { SearchIcon } from './SearchIcon' import crypto from 'crypto' +import router from 'next/router' /* Copied from SearchBoxProvided in react-instantsearch-dom */ interface SearchBoxProps { @@ -39,6 +40,16 @@ export default connectSearchBox( debouncedSearch(e) } + const handleKeyDown = (e) => { + if (e.key === 'Enter') { + const searchTerm = e.target.value; + if (searchTerm.trim()) + { + router.push(`/search?query=${encodeURIComponent(searchTerm)}`); + } + } + } + return ( diff --git a/components/tinaMarkdownComponents/recipeComponents.tsx b/components/tinaMarkdownComponents/recipeComponents.tsx index e10061af4..098c00221 100644 --- a/components/tinaMarkdownComponents/recipeComponents.tsx +++ b/components/tinaMarkdownComponents/recipeComponents.tsx @@ -12,6 +12,9 @@ export const codeBlockComponents = { Prism.highlightAll(); }, []); + + + return (
 
         
diff --git a/pages/search.tsx b/pages/search.tsx
new file mode 100644
index 000000000..81a402de3
--- /dev/null
+++ b/pages/search.tsx
@@ -0,0 +1,80 @@
+import { GetStaticProps } from 'next';
+import client from 'tina/__generated__/client';
+import getTableOfContents from 'utils/docs/getTableOfContents';
+import { Layout } from 'components/layout';
+import { useRouter } from 'next/router';
+import { formatTableofContentsData } from 'utils/docs/getDocProps';
+
+import {
+  SearchHeader,
+  SearchTabs,
+} from 'components/docsSearch/SearchComponent';
+import { LeftHandSideParentContainer } from 'components/docsSearch/SearchNavigation';
+
+const DocsSearchPage = ({
+  tableOfContents,
+  formatted,
+}: {
+  tableOfContents: any;
+  formatted: any;
+}) => {
+  const router = useRouter();
+  const searchQuery = router.query.query as string;
+
+  return (
+    
+      
+
+
+ +
+
+ + +
+
+
+
+ ); +}; + +export default DocsSearchPage; + +export const getStaticProps: GetStaticProps = async function () { + try { + const slug = 'index'; + const results = await client.queries.doc({ relativePath: `${slug}.mdx` }); + const doc_data = results.data.doc; + const query = ` + query { + docsTableOfContents(relativePath: "docs-toc.json") { + _values + } + } + `; + const tableOfContents = getTableOfContents(doc_data.body.children); + const docTocData = await client.request( + { + query, + variables: { relativePath: 'docs-toc.json' }, + }, + {} + ); + const formatted = formatTableofContentsData(docTocData, null); + + return { + props: { + tableOfContents, + formatted, + }, + }; + } catch (e) { + console.error('Error fetching Table of Contents or Docs Navigation:', e); + return { + props: { + tableOfContents: null, + docsNavigation: null, + }, + }; + } +}; diff --git a/utils/new-search.tsx b/utils/new-search.tsx new file mode 100644 index 000000000..ea09bf1f8 --- /dev/null +++ b/utils/new-search.tsx @@ -0,0 +1,36 @@ +import algoliasearch from 'algoliasearch'; + +const DEFAULT_ALGOLIA_APP_ID = '80HKRA52OJ'; +const DEFAULT_ALGOLIA_SEARCH_KEY = 'f13c10ad814c92b85f380deadc2db2dc'; + +const searchClient = algoliasearch( + process.env.GATSBY_ALGOLIA_APP_ID || DEFAULT_ALGOLIA_APP_ID, + (process.env.GATSBY_ALGOLIA_SEARCH_KEY || DEFAULT_ALGOLIA_SEARCH_KEY) as string +); + +interface SearchResults { + docs: { results: any[]; count: number }; + blogs: { results: any[]; count: number }; +} + +export const fetchAlgoliaSearchResults = async (query: string): Promise => { + try { + + const [docsResults, blogsResults] = await Promise.all([ + searchClient.initIndex('Tina-Docs-Next').search(query, { hitsPerPage: 50 }), + searchClient.initIndex('Tina-Blogs-Next').search(query, { hitsPerPage: 50 }), + ]); + + + return { + docs: { results: docsResults.hits, count: docsResults.nbHits }, + blogs: { results: blogsResults.hits, count: blogsResults.nbHits }, + }; + } catch (error) { + console.error('Error fetching Algolia search results:', error); + return { + docs: { results: [], count: 0 }, + blogs: { results: [], count: 0 }, + }; + } +};