diff --git a/content/docs/analytics/google-analytics.md b/content/docs/analytics/google-analytics.md index 98ebe86f7..18174d089 100644 --- a/content/docs/analytics/google-analytics.md +++ b/content/docs/analytics/google-analytics.md @@ -59,7 +59,7 @@ If you only want to track your searchbox and you are using the [DataSearch](http ### ReactiveSearch Vue If you want to track your searchbox and you are using the [data-search](https://docs.appbase.io/docs/reactivesearch/vue/search/DataSearch/) component for it, you can do the following. -```vue +```html ` | `search`,`suggestion` | false | -> Note: The `fieldWeights` property has been marked as deprecated in v7.47.0 and would be removed in the next major version of appbase.io. We recommend you to use the [dataField](/docs/search/reactivesearch-api/reference/#datafield) property to define the weights. +> Note: The `fieldWeights` property has been marked as deprecated in v7.47.0 and would be removed in the next major version of reactivesearch.io. We recommend you to use the [dataField](/docs/search/reactivesearch-api/reference/#datafield) property to define the weights. ### nestedField @@ -572,7 +572,7 @@ Useful for showing the correct results for an incorrect search parameter by taki |

Type

|

Applicable on query of type

|

Required

| | -------------- | --------------------------- | -------- | -| `int | string` | `search`, `suggestion` | false | +| `int, string` | `search`, `suggestion` | false | > Note: > @@ -1583,6 +1583,21 @@ When featured suggestions are enabled, set the value of the `searchboxId` to use **Supported Engines** elasticsearch, opensearch +### Compound Clause + +**Supported Engines** +opensearch, elasticsearch + +### enableDocumentSuggestions + +**Supported Engines** +elasticsearch, opensearch + +### documentSuggestionsConfig + +**Supported Engines** +elasticsearch, opensearch + ## settings **Supported Engines** @@ -1593,21 +1608,21 @@ Not dependent on engine, works for all. **Supported Engines** elasticsearch, solr, opensearch -`bool` defaults to `false`. If `true` then it'll enable the recording of Appbase.io analytics. +`bool` defaults to `false`. If `true` then it'll enable the recording of ReactiveSearch.io analytics. ### userId **Supported Engines** elasticsearch, opensearch -`String` It allows you to define the user id which will be used to record the Appbase.io analytics. +`String` It allows you to define the user id which will be used to record the ReactiveSearch.io analytics. ### customEvents **Supported Engines** elasticsearch, opensearch -`Object` It allows you to set the custom events which can be used to build your own analytics on top of the Appbase.io analytics. Further, these events can be used to filter the analytics stats from the Appbase.io dashboard. In the below example, we\'re setting up two custom events that will be recorded with each search request. +`Object` It allows you to set the custom events which can be used to build your own analytics on top of the ReactiveSearch.io analytics. Further, these events can be used to filter the analytics stats from the ReactiveSearch.io dashboard. In the below example, we\'re setting up two custom events that will be recorded with each search request. ```js { diff --git a/content/docs/search/reactivesearch-api/reference/index.md b/content/docs/search/reactivesearch-api/reference/index.md index e8371f5a8..5ce75ab96 100644 --- a/content/docs/search/reactivesearch-api/reference/index.md +++ b/content/docs/search/reactivesearch-api/reference/index.md @@ -242,7 +242,7 @@ For example, the below query has two data fields defined and each field has a di | ------------ | --------------------------- | -------- | | `Array` | `search`,`suggestion` | false | -> Note: The `fieldWeights` property has been marked as deprecated in v7.47.0 and would be removed in the next major version of appbase.io. We recommend you to use the [dataField](/docs/search/reactivesearch-api/reference/#datafield) property to define the weights. +> Note: The `fieldWeights` property has been marked as deprecated in v7.47.0 and would be removed in the next major version of reactivesearch.io. We recommend you to use the [dataField](/docs/search/reactivesearch-api/reference/#datafield) property to define the weights. ### nestedField @@ -573,7 +573,7 @@ Useful for showing the correct results for an incorrect search parameter by taki |

Type

|

Applicable on query of type

|

Required

| | -------------- | --------------------------- | -------- | -| `int | string` | `search`, `suggestion` | false | +| `int, string` | `search`, `suggestion` | false | > Note: > @@ -1584,6 +1584,21 @@ When featured suggestions are enabled, set the value of the `searchboxId` to use **Supported Engines** elasticsearch, opensearch +### Compound Clause + +**Supported Engines** +opensearch, elasticsearch + +### enableDocumentSuggestions + +**Supported Engines** +elasticsearch, opensearch + +### documentSuggestionsConfig + +**Supported Engines** +elasticsearch, opensearch + ## settings **Supported Engines** @@ -1594,21 +1609,21 @@ Not dependent on engine, works for all. **Supported Engines** elasticsearch, solr, opensearch -`bool` defaults to `false`. If `true` then it'll enable the recording of Appbase.io analytics. +`bool` defaults to `false`. If `true` then it'll enable the recording of ReactiveSearch.io analytics. ### userId **Supported Engines** elasticsearch, opensearch -`String` It allows you to define the user id which will be used to record the Appbase.io analytics. +`String` It allows you to define the user id which will be used to record the ReactiveSearch.io analytics. ### customEvents **Supported Engines** elasticsearch, opensearch -`Object` It allows you to set the custom events which can be used to build your own analytics on top of the Appbase.io analytics. Further, these events can be used to filter the analytics stats from the Appbase.io dashboard. In the below example, we\'re setting up two custom events that will be recorded with each search request. +`Object` It allows you to set the custom events which can be used to build your own analytics on top of the ReactiveSearch.io analytics. Further, these events can be used to filter the analytics stats from the ReactiveSearch.io dashboard. In the below example, we\'re setting up two custom events that will be recorded with each search request. ```js { diff --git a/content/docs/search/reactivesearch-api/reference/mongodb.md b/content/docs/search/reactivesearch-api/reference/mongodb.md index 2c1a19a5d..03ef2acd1 100644 --- a/content/docs/search/reactivesearch-api/reference/mongodb.md +++ b/content/docs/search/reactivesearch-api/reference/mongodb.md @@ -480,7 +480,7 @@ Useful for showing the correct results for an incorrect search parameter by taki |

Type

|

Applicable on query of type

|

Required

| | -------------- | --------------------------- | -------- | -| `int | string` | `search`, `suggestion` | false | +| `int, string` | `search`, `suggestion` | false | > Note: > diff --git a/content/docs/search/reactivesearch-api/reference/opensearch.md b/content/docs/search/reactivesearch-api/reference/opensearch.md index f2b345a2d..69b503c8d 100644 --- a/content/docs/search/reactivesearch-api/reference/opensearch.md +++ b/content/docs/search/reactivesearch-api/reference/opensearch.md @@ -241,7 +241,7 @@ For example, the below query has two data fields defined and each field has a di | ------------ | --------------------------- | -------- | | `Array` | `search`,`suggestion` | false | -> Note: The `fieldWeights` property has been marked as deprecated in v7.47.0 and would be removed in the next major version of appbase.io. We recommend you to use the [dataField](/docs/search/reactivesearch-api/reference/#datafield) property to define the weights. +> Note: The `fieldWeights` property has been marked as deprecated in v7.47.0 and would be removed in the next major version of reactivesearch.io. We recommend you to use the [dataField](/docs/search/reactivesearch-api/reference/#datafield) property to define the weights. ### nestedField @@ -572,7 +572,7 @@ Useful for showing the correct results for an incorrect search parameter by taki |

Type

|

Applicable on query of type

|

Required

| | -------------- | --------------------------- | -------- | -| `int | string` | `search`, `suggestion` | false | +| `int, string` | `search`, `suggestion` | false | > Note: > @@ -1614,6 +1614,21 @@ The fields supported by the AIConfig object are: - **minTokens**: Minimum number of tokens to generate in the response. Whenever possible, max tokens is respected, however when the input context + max tokens combined exceed the model limit, the min tokens value is used to calibrate for an optimum output token. Defaults to 100. - **temperature**: A control for randomness, a lower value implies a more deterministic output. Defaults to 1, valid values are between [0, 2]. +### Compound Clause + +**Supported Engines** +opensearch, elasticsearch + +### enableDocumentSuggestions + +**Supported Engines** +elasticsearch, opensearch + +### documentSuggestionsConfig + +**Supported Engines** +elasticsearch, opensearch + ## settings **Supported Engines** @@ -1624,21 +1639,21 @@ Not dependent on engine, works for all. **Supported Engines** elasticsearch, solr, opensearch -`bool` defaults to `false`. If `true` then it'll enable the recording of Appbase.io analytics. +`bool` defaults to `false`. If `true` then it'll enable the recording of ReactiveSearch.io analytics. ### userId **Supported Engines** elasticsearch, opensearch -`String` It allows you to define the user id which will be used to record the Appbase.io analytics. +`String` It allows you to define the user id which will be used to record the ReactiveSearch.io analytics. ### customEvents **Supported Engines** elasticsearch, opensearch -`Object` It allows you to set the custom events which can be used to build your own analytics on top of the Appbase.io analytics. Further, these events can be used to filter the analytics stats from the Appbase.io dashboard. In the below example, we\'re setting up two custom events that will be recorded with each search request. +`Object` It allows you to set the custom events which can be used to build your own analytics on top of the ReactiveSearch.io analytics. Further, these events can be used to filter the analytics stats from the ReactiveSearch.io dashboard. In the below example, we\'re setting up two custom events that will be recorded with each search request. ```js { diff --git a/content/docs/search/reactivesearch-api/reference/solr.md b/content/docs/search/reactivesearch-api/reference/solr.md index 1690f4515..c640e5421 100644 --- a/content/docs/search/reactivesearch-api/reference/solr.md +++ b/content/docs/search/reactivesearch-api/reference/solr.md @@ -924,7 +924,7 @@ Not dependent on engine, works for all. **Supported Engines** elasticsearch, solr, opensearch -`bool` defaults to `false`. If `true` then it'll enable the recording of Appbase.io analytics. +`bool` defaults to `false`. If `true` then it'll enable the recording of ReactiveSearch.io analytics. ### backend diff --git a/gatsby-config.js b/gatsby-config.js index de4baf1ed..00ea242c8 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -56,7 +56,6 @@ const plugins = [ html: 'html', sh: 'bash', curl: 'bash', - vue: 'vue', }, }, }, diff --git a/package.json b/package.json index ec3e06d5f..5a3063413 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "dependencies": { "@appbaseio/designkit": "^0.13.0", "@appbaseio/reactivemaps": "^3.0.0", - "@appbaseio/reactivesearch": "^3.30.3", + "@appbaseio/reactivesearch": "^4.1.0-alpha.11", "@loadable/component": "^5.15.2", "@reach/router": "^1.2.1", "@typeform/embed": "^1.34.1", @@ -115,6 +115,7 @@ "react-dom": "^16.9.0", "react-dropdown": "^1.6.4", "react-helmet": "^6.0.0", + "react-icons": "^4.10.1", "react-modal": "3.8.1", "react-responsive-modal": "^4.0.1", "react-router": "^6.0.1", diff --git a/src/components/common/NavBar.js b/src/components/common/NavBar.js index a97674e10..7c929d382 100644 --- a/src/components/common/NavBar.js +++ b/src/components/common/NavBar.js @@ -2,13 +2,20 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'gatsby'; import Button from '@appbaseio/designkit/lib/atoms/Button'; +import { ReactiveBase, SearchBox } from '@appbaseio/reactivesearch'; import ThemeSwitch from './themeSwitch'; import { Spirit } from '../../styles/spirit-styles'; import Logo from './ReactivesearchLogo'; import DropdownLink from './DropdownLink'; import Icon from './Icon'; -import Search from './search/HomeSearch'; import MobileNav from './MobileNav'; +import { CREDENTIAL_FOR_DOCS, SEARCH_COMPONENT_ID } from './constants'; + +import * as styles from './NavBar.module.css'; +import { Suggestion } from './search/Suggestion'; +import { DocumentSuggestion } from './search/DocumentSuggestions'; +import { transformRequest } from './transformRequest'; +import { transformResponse } from './transformResponse'; const NavBar = ({ theme, setThemeType, themeType }) => { // Theme definitions @@ -46,7 +53,7 @@ const NavBar = ({ theme, setThemeType, themeType }) => { }, [mockWindow]); return ( -
+ <>
- + + { + const suggestionType = suggestion._suggestion_type; + + return suggestionType === 'index' || + suggestionType === 'document' ? ( + + ) : ( + + ); + }} + /> +
{mockWindow?.innerWidth > 768 ? ( @@ -503,7 +566,7 @@ const NavBar = ({ theme, setThemeType, themeType }) => { - + ); }; diff --git a/src/components/common/NavBar.module.css b/src/components/common/NavBar.module.css new file mode 100644 index 000000000..3faad4e48 --- /dev/null +++ b/src/components/common/NavBar.module.css @@ -0,0 +1,38 @@ +.logo { + height: 75px; +} + +@media screen and (max-width: 800px) { + .logo { + height: 60px; + } +} +@media screen and (max-width: 500px) { + .logo { + height: 50px; + } +} + +.resourceFilter a { + text-decoration: none; + font-family: 'Raleway', sans-serif; + font-weight: bolder; + text-transform: uppercase; + color: black; + padding: 1.25rem 2rem; +} + +.searchBox { + font-size: 18px; +} +.searchBox ol, +.searchBox ul, +.searchBox dl { + padding-left: 0px; +} + +.searchBox textarea { + overflow-y: hidden; + font-size: 1em !important; + resize: none; +} diff --git a/src/components/common/constants.js b/src/components/common/constants.js new file mode 100644 index 000000000..53f7967e5 --- /dev/null +++ b/src/components/common/constants.js @@ -0,0 +1,2 @@ +export const SEARCH_COMPONENT_ID = 'search'; +export const CREDENTIAL_FOR_DOCS = 'be8a0649fdf2:61fdfec3-d5d2-43c9-9506-8857ec3136ac'; diff --git a/src/components/common/search/DocumentSuggestions.css b/src/components/common/search/DocumentSuggestions.css new file mode 100644 index 000000000..703ff390d --- /dev/null +++ b/src/components/common/search/DocumentSuggestions.css @@ -0,0 +1,3 @@ +li.active-li-item .search__suggestion .search__suggestionIcon svg { + fill: var(--bs-primary) !important; +} diff --git a/src/components/common/search/DocumentSuggestions.js b/src/components/common/search/DocumentSuggestions.js new file mode 100644 index 000000000..6aea874f1 --- /dev/null +++ b/src/components/common/search/DocumentSuggestions.js @@ -0,0 +1,93 @@ +import React from 'react'; +import { object } from 'prop-types'; +import { navigate } from 'gatsby'; + +import './DocumentSuggestions.css'; +import { URLIcon } from './URLIcon'; +import { getBreadcrumbText } from './getBreadcrumbText'; + +import * as styles from './DocumentSuggestions.module.css'; + +function sanitizeHTMLAndCombineStrings(inputStrings) { + // Combine all input strings into a single string + const combinedString = inputStrings.join(' '); + // Remove HTML tags using a regular expression + const withoutTags = combinedString.replace(/<[^>]+>/g, ''); + + // Remove multiple consecutive spaces and newlines + let finalString = withoutTags.replace(/\s{2,}/g, ' ').trim(); + + if (finalString && finalString.startsWith('#')) { + finalString = finalString + .split('>') + .slice(1) + .join('>'); + } + return finalString; +} + +// eslint-disable-next-line +export const DocumentSuggestion = ({ source, docId }) => { + const breadcrumbText = getBreadcrumbText(source.url); + const isMobileWidth = window.innerWidth < 500; + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + { + // to stop the search from populating value + e.stopPropagation(); + navigate(`${source.url}` || '#', { state: { docId } }); + }} + > +
+
+
+ +
+
+
+ {source.title || source.meta_title} +
+ {breadcrumbText && isMobileWidth ? null : ( +
+ + {breadcrumbText} + +
+ )} +
+ {source.meta_description ? ( + source.meta_description + ) : ( +
+ )} +
+
+
+
+
+ ); +}; + +DocumentSuggestion.defaultProps = { + source: {}, +}; + +DocumentSuggestion.propTypes = { + source: object, +}; diff --git a/src/components/common/search/DocumentSuggestions.module.css b/src/components/common/search/DocumentSuggestions.module.css new file mode 100644 index 000000000..c072f75be --- /dev/null +++ b/src/components/common/search/DocumentSuggestions.module.css @@ -0,0 +1,97 @@ +.spinner { + position: absolute; + height: 80vh; + width: 100vw; + left: 0px; + display: flex; + justify-content: center; + padding-top: 50px; + z-index: 1; + background-color: white; +} +.headingTag { + background: linear-gradient( + 30deg, + rgb(59, 130, 246) 0%, + rgb(59, 130, 246) 0%, + rgb(255, 42, 111) 100% + ); + margin-right: auto; + border-radius: var(--bs-border-radius); + padding: 5px 10px; +} +.suggestions { + position: absolute; + z-index: 2; + box-shadow: #9597a1 0px 8px 24px; + background: white; + width: 100%; + border-radius: 0px 0px 10px 10px; + max-height: 500px; + overflow: auto; + font-size: 0.9em !important; +} +.suggestionHeading { + font-size: 0.8em !important; + font-weight: bold; +} +.suggestion { + font-size: 12px; + font-weight: normal; + padding: 5px 10px; + width: 100%; + height: 100%; + display: block; + position: relative; + text-decoration: none !important; + color: inherit !important; +} +.suggestionTitle { + text-transform: uppercase; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.suggestionBreadcrumb { + padding: 0.25rem; + margin-top: 0.25rem; + font-size: 0.8em !important; + position: absolute; + top: 2px; + max-width: 100%; + width: max-content; + right: 10px; + background-color: #eee; + color: #6c757d; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-radius: 2px; +} +.suggestionDescription { + margin-top: 0.25rem; + width: 100%; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} +@media (max-width: 500px) { + .suggestionBreadcrumb { + position: relative; + top: 0; + right: 0; + font-size: 0.7em !important; + display: block; + } + .suggestionTitle { + width: 100%; + } +} +.suggestionIcon { + margin-right: 15px; +} +.selectedSuggestion { + font-weight: bold; +} diff --git a/src/components/common/search/HomeSearch.js b/src/components/common/search/HomeSearch.js deleted file mode 100644 index 328cd6c98..000000000 --- a/src/components/common/search/HomeSearch.js +++ /dev/null @@ -1,451 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { navigate, Link } from 'gatsby'; -import Fuse from 'fuse.js'; -import Autosuggest from 'react-autosuggest'; -import hotkeys from 'hotkeys-js'; -import groupBy from 'lodash/groupBy'; -import orderBy from 'lodash/orderBy'; -import data from '../../../data/search.index.json'; -import { Spirit } from '../../../styles/spirit-styles'; -import Icon from '../Icon'; -import sidebar from '../../../data/sidebars/all-sidebar'; -import '../../../styles/custom.css'; - -const options = { - includeScore: true, - includeMatches: true, - ignoreLocation: true, - keys: [ - { name: 'title', weight: 0.65 }, - { name: 'tokens[0]', weight: 0.3 }, - { name: 'heading', weight: 0.05 }, - ], -}; - -const getSection = url => { - const isHavingHash = url.indexOf('#'); - let link = url; - let subSection = ''; - if (isHavingHash) { - link = url.split('#')[0]; - subSection = url.split('#')[1]; - if (subSection && subSection.includes('">')) subSection = subSection.split('">')[0]; - } - if (link.startsWith('/docs/reactivesearch')) { - const linkTags = link.split('/'); - const sectionName = linkTags[linkTags.length - 3]; - let techName = linkTags[linkTags.length - 4]; - - switch (techName) { - case 'v2': - techName = 'React v2'; - break; - case 'v3': - techName = 'React v3'; - break; - case 'v4': - techName = "React v4"; - break; - default: - } - - if ( - [ - 'components', - 'advanced', - 'overview', - 'ui-builder', - 'vue-searchbox', - 'react-searchbox', - 'react-native-searchbox', - 'flutter-searchbox', - 'flutter-searchbox-ui', - 'searchbase', - 'searchbase-dart', - 'atlas-search', - 'autocomplete-plugin', - ].indexOf(sectionName.toLowerCase()) !== -1 - ) { - return subSection - ? `${techName} > ${sectionName} > ${subSection}` - : `${techName} > ${sectionName}`; - } - - return subSection - ? `${techName} > ${sectionName} Components > ${subSection}` - : `${techName} > ${sectionName} Components`; - } - const foundItem = sidebar.find(item => item.link === link || link.startsWith(item.link)); - - if (foundItem) { - return subSection ? `${foundItem.topic} > ${subSection}` : foundItem.topic; - } - - return ''; -}; - -const getValue = url => { - if (url.startsWith('/docs/reactivesearch/v2')) { - return 'react-bw'; - } - if (url.startsWith('/docs/reactivesearch/react/v3')) { - return 'react-bw'; - } - if (url.startsWith('/docs/reactivesearch/react')) { - return 'react-bw'; - } - if (url.startsWith('/docs/reactivesearch/react')) { - return 'react-bw'; - } - if (url.startsWith('/docs/reactivesearch/vue')) { - return 'vue-bw'; - } - if (url.startsWith('/docs/reactivesearch/native')) { - return 'native-bw'; - } - if (url.startsWith('/docs/gettingstarted')) { - return 'gettingStarted'; - } - if (url.startsWith('/docs/analytics')) { - return 'analytics'; - } - if (url.startsWith('/api/js')) { - return 'js-bw'; - } - if (url.startsWith('/api/rest')) { - return 'rest'; - } - if (url.startsWith('/docs/data')) { - return 'importData'; - } - if (url.startsWith('/docs/security')) { - return 'security'; - } - - return 'buildingUI'; -}; - -const HitTemplate = ({ hit, currentValue }) => { - const sectionName = getSection(hit.url); - - const highlightedTitle = hit.title.replace(new RegExp(currentValue, 'ig'), matched => { - return `${matched}`; - }); - const tokens = hit.tokens; - let highlightedToken = - tokens[0] && - tokens[0].replace(new RegExp(currentValue, 'ig'), matched => { - return `${matched}`; - }); - if (highlightedToken && highlightedToken.startsWith('#')) { - highlightedToken = highlightedToken - .split('>') - .slice(1) - .join('>'); - } - - return ( - -
-
- -
- -
-
-
- {sectionName ? ( -
- ) : null} -
-

-

- {!currentValue ? ( - - - - - ) : null} -
- - ); -}; - -class AutoComplete extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: '', - showContainer: false, - hits: this.getSuggestions(''), - hasMounted: false, - }; - - this.onChange = this.onChange.bind(this); - this.onSuggestionsUpdateRequested = this.onSuggestionsUpdateRequested.bind(this); - this.shouldRenderSuggestions = this.shouldRenderSuggestions.bind(this); - this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this); - this.renderSuggestion = this.renderSuggestion.bind(this); - this.getSuggestionValue = this.getSuggestionValue.bind(this); - } - - componentDidMount() { - this.setState({ - hasMounted: true, - }); - hotkeys('/', function(event, handler) { - // Prevent the default refresh event under WINDOWS system - event.preventDefault(); - document.querySelector("[data-cy='search-input']").focus(); - }); - } - - getSectionsMapper = url => { - if (url.includes('react')) return 'react'; - - if (url.includes('vue')) return 'vue'; - - if (url.includes('native')) return 'native'; - - if (url.includes('relevancy')) return 'relevancy'; - - return 'default'; - }; - - searchWithFuse = inputValue => { - const fuse = new Fuse(data, options); - const searchValue = fuse.search(inputValue); - - return searchValue; - }; - - filterPageSepecificResults = (pageSpecificResults, visitedPages = []) => { - let arr = []; - Object.keys(pageSpecificResults).forEach(url => { - if (!visitedPages.includes(url)) - arr = [...arr, ...pageSpecificResults[url].slice(0, 2)]; - }); - - return arr; - }; - - getSuggestions = value => { - const { isMobile } = this.props; - const noOfSuggestions = 40; - const inputValue = value.trim().toLowerCase(); - const inputLength = inputValue.length; - const searchValue = this.searchWithFuse(inputValue) - .map(res => ({ - ...res, - ...res.item, - baseURL: res?.item?.url.split('#')[0] || '', - section: this.getSectionsMapper(res?.item?.url), - })) - .filter(item => !item.url.startsWith('/docs/reactivesearch/v2')) - .filter(item => !item.url.startsWith('/docs/reactivesearch/react/v3')) - .filter(item => !item.url.startsWith('/docs/reactivesearch/vue/v1')) - .filter(item => item.url !== '/data-schema/'); - let topResults = searchValue.slice(0, noOfSuggestions); - - const exactMatchIndex = topResults.findIndex( - item => item.title.toLowerCase() === inputValue && !item.heading, - ); - if (exactMatchIndex > 0) { - topResults = [ - topResults[exactMatchIndex], - ...topResults.slice(0, exactMatchIndex), - ...topResults.slice(exactMatchIndex + 1), - ]; - } - - const newTopResults = orderBy(topResults, res => res.score); - let visitedPages = []; - const groupedByScore = groupBy(newTopResults, res => res.score); - let transformedHits = []; - Object.keys(groupedByScore).forEach(score => { - const pageSpecificResults = groupBy(groupedByScore[score], res => res.baseURL); - - const grouped = groupBy( - this.filterPageSepecificResults(pageSpecificResults, visitedPages), - res => res.section, - ); - const newHits = [ - ...(grouped['react'] || []), - ...(grouped['vue'] || []), - ...(grouped['native'] || []), - ...(grouped['relevancy'] || []), - ...(grouped['default'] || []), - ]; - transformedHits = [...transformedHits, ...newHits]; - visitedPages = [...visitedPages, ...Object.keys(pageSpecificResults)]; - }); - - return inputLength === 0 - ? JSON.parse( - typeof window !== 'undefined' - ? localStorage.getItem('recentSuggestions') || '[]' - : '[]', - ) - : transformedHits.slice(0, isMobile ? 5 : 20); - }; - - onChange(event, { newValue, method }) { - this.setState({ - value: newValue, - }); - } - - onSuggestionsUpdateRequested({ value }) { - const suggestions = this.getSuggestions(value); - this.setState({ - hits: suggestions, - }); - } - - onSuggestionsFetchRequested({ value }) { - const suggestions = this.getSuggestions(value); - this.setState({ - hits: suggestions, - }); - } - - shouldRenderSuggestions() { - return true; - } - - getSuggestionValue = hit => { - return hit.title; - }; - - suggestionSelected = (event, { suggestion }) => { - if (event.key === 'Enter') { - navigate(suggestion.url); - } - }; - - renderSuggestion = hit => { - const { value } = this.state; - return ; - }; - - renderSuggestionsContainer = ({ containerProps, children, query }) => { - const { value, showContainer } = this.state; - return ( -
-
{children}
- {showContainer ? ( -
-
↑↓ Navigate
-
↩ Go
-
- ) : null} -
- ); - }; - - enableFocus = () => { - document.querySelector("[data-cy='search-input']").focus(); - }; - - onFocus = () => { - this.setState({ - showContainer: true, - }); - }; - - onBlur = () => { - this.setState({ - showContainer: false, - }); - }; - - onKeyDown = e => { - if (e.keyCode === 27) { - document.querySelector("[data-cy='search-input']").blur(); - } - }; - - render() { - // Don't show sections with no results - const { hits, value, hasMounted } = this.state; - const inputProps = { - placeholder: `Search documentation...`, - onChange: this.onChange, - value, - onFocus: this.onFocus, - onBlur: this.onBlur, - onKeyDown: this.onKeyDown, - 'data-cy': `search-input`, - }; - - const inputTheme = `input-reset form-text b--transparent search-modal-field-bg br-pill flex-auto whitney lh-normal pa2 pl8 plr3 w-100 dark-placeholder`; - - const theme = { - input: `${inputTheme} home-input`, - inputOpen: inputTheme, - inputFocused: inputTheme, - suggestionsContainerOpen: `fixed home-search`, - suggestionHighlighted: 'highlighted-suggestion', - suggestionsList: `list pa0 ma0 pt1 flex-auto`, - sectionContainer: `pb4 mb4`, - sectionTitle: `f8 lh-h4 fw5 midgrey w30 tr mt2 sticky top-2 pr2`, - }; - - if (!hasMounted) { - return null; - } - - return ( - <> - - - - - ); - } -} - -AutoComplete.defaultProps = { - isMobile: false, -}; - -AutoComplete.propTypes = { - isMobile: PropTypes.bool, -}; - -export default AutoComplete; diff --git a/src/components/common/search/Search.js b/src/components/common/search/Search.js deleted file mode 100644 index 25a60e617..000000000 --- a/src/components/common/search/Search.js +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import Link from 'gatsby-link'; -import * as JsSearch from 'js-search'; -import Autosuggest from 'react-autosuggest'; -import data from '../../../data/search.index.json'; -import { Spirit } from '../../../styles/spirit-styles'; - -const search = new JsSearch.Search('url'); -search.tokenizer = new JsSearch.StopWordsTokenizer(new JsSearch.SimpleTokenizer()); - -search.addIndex('title'); -search.addIndex('tag'); -search.addDocuments(data); - -const getSuggestions = value => { - const inputValue = value.trim().toLowerCase(); - const inputLength = inputValue.length; - let topResults = search.search(inputValue).slice(0, 8); - const exactMatchIndex = topResults.findIndex( - item => item.title.toLowerCase() === inputValue && !item.heading.length, - ); - if (exactMatchIndex > 0) { - topResults = [ - topResults[exactMatchIndex], - ...topResults.slice(0, exactMatchIndex), - ...topResults.slice(exactMatchIndex + 1), - ]; - } - return inputLength === 0 ? [] : topResults; -}; - -const HitTemplate = ({ hit }) => ( - -

{hit.mate_title || hit.title}

-

- -); - -class AutoComplete extends React.Component { - constructor(props) { - super(props); - - this.state = { - value: '', - hits: [], - }; - - this.onChange = this.onChange.bind(this); - this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this); - this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this); - this.renderSuggestion = this.renderSuggestion.bind(this); - } - - onChange(event, { newValue }) { - this.setState(() => { - return { value: newValue }; - }); - } - - onSuggestionsFetchRequested({ value }) { - const suggestions = getSuggestions(value); - this.setState({ - hits: suggestions, - }); - } - - onSuggestionsClearRequested() { - this.setState({ - hits: [], - }); - } - - renderSuggestion(hit) { - return ; - } - - render() { - // Don't show sections with no results - const { hits, value } = this.state; - - const inputProps = { - placeholder: `Search documentation...`, - onChange: this.onChange, - value, - autoFocus: true, - 'data-cy': `search-input`, - }; - - const inputTheme = `input-reset form-text b--transparent search-modal-field-bg br-pill flex-auto whitney lh-normal pa2 pl8 plr3 w-100 dark-placeholder`; - - const theme = { - input: inputTheme, - inputOpen: inputTheme, - inputFocused: inputTheme, - suggestionsContainerOpen: `pa11 pt3 pb3 mt10 bt b--whitegrey nl10 nr10 nb10 search-modal-result-container`, - suggestionsList: `list pa0 ma0 pt1 flex-auto`, - sectionContainer: `pb4 mb4`, - sectionTitle: `f8 lh-h4 fw5 midgrey w30 tr mt2 sticky top-2 pr2`, - }; - - return ( - <> - - - ); - } -} - -export default AutoComplete; diff --git a/src/components/common/search/SearchInput.js b/src/components/common/search/SearchInput.js deleted file mode 100644 index 1a0e8ed26..000000000 --- a/src/components/common/search/SearchInput.js +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Icon from '../Icon'; - -export const SearchInput = ({ theme, isHome, onClick }) => { - if (isHome) { - return ( -

- - - -
- ); - } - if (theme) { - return ( -
- - - -
- ); - } - - return null; -}; - -SearchInput.propTypes = { - isHome: PropTypes.bool, - theme: PropTypes.shape({ - icon: PropTypes.string, - searchBox: PropTypes.string, - }), - onClick: PropTypes.func.isRequired, -}; - -export default SearchInput; diff --git a/src/components/common/search/SearchModal.js b/src/components/common/search/SearchModal.js deleted file mode 100644 index 80ce73b5b..000000000 --- a/src/components/common/search/SearchModal.js +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import Modal from 'react-modal'; -import PropTypes from 'prop-types'; - -import Search from './Search'; -import SearchInput from './SearchInput'; -import Icon from '../Icon'; - -class SearchModal extends React.Component { - constructor(props) { - super(props); - - this.state = { - modalIsOpen: false, - }; - - this.openModal = this.openModal.bind(this); - this.closeModal = this.closeModal.bind(this); - } - - openModal() { - this.setState(() => { - return { modalIsOpen: true }; - }); - } - - closeModal() { - this.setState(() => { - return { modalIsOpen: false }; - }); - } - - componentDidMount() { - Modal.setAppElement(`#___gatsby`); - } - - render() { - return ( - <> - - -
- -
-
- - - -
-
- - ); - } -} - -SearchModal.defaultProps = { - isHome: false, -}; - -SearchModal.propTypes = { - theme: PropTypes.shape({ - icon: PropTypes.string, - searchBox: PropTypes.string, - }), - isHome: PropTypes.bool, -}; - -export default SearchModal; diff --git a/src/components/common/search/Suggestion.js b/src/components/common/search/Suggestion.js new file mode 100644 index 000000000..a73d1da4e --- /dev/null +++ b/src/components/common/search/Suggestion.js @@ -0,0 +1,25 @@ +import React from 'react'; + +import { object } from 'prop-types'; + +import { BsClock, BsLightningCharge } from 'react-icons/bs'; + +export const Suggestion = ({ suggestion }) => { + let type = suggestion._suggestion_type || 'normal'; + let Icon; + if (type === 'recent') { + Icon = BsClock; + } else if (type === 'popular') { + Icon = BsLightningCharge; + } + return ( +
+ {Icon ? : null} + {suggestion.label} +
+ ); +}; + +Suggestion.propTypes = { + suggestion: object, +}; diff --git a/src/components/common/search/URLIcon.js b/src/components/common/search/URLIcon.js new file mode 100644 index 000000000..ce5b86219 --- /dev/null +++ b/src/components/common/search/URLIcon.js @@ -0,0 +1,128 @@ +import React from 'react'; +import { number, object, string } from 'prop-types'; +import { + BsGlobe, + BsLightningChargeFill, + BsNewspaper, + BsPieChartFill, + BsReverseListColumnsReverse, + BsRocketTakeoffFill, + BsSearch, + BsShieldFillCheck, +} from 'react-icons/bs'; + +import * as styles from './URLIcon.module.css'; + +// eslint-disable-next-line +export function URLIcon({ url, style = {}, size = 50 }) { + if (url) { + if (url.match('https://www.reactivesearch.io/')) { + return ; + } + if (url.match('https://blog.reactivesearch.io')) { + return ; + } + if (url.match('/docs/')) { + if (url.match('/docs/reactivesearch/react')) { + return ( + + ); + } + if (url.match('/docs/reactivesearch/vue')) { + return ( + + ); + } + if (url.match('/docs/reactivesearch/flutter')) { + return ( + + ); + } + if (url.match('/docs/pipelines')) { + return ( + + ); + } + if (url.match('/docs/speed')) { + return ( + + ); + } + if (url.match('/docs/hosting')) { + return ( + + ); + } + if (url.match('/docs/security')) { + return ( + + ); + } + + if (url.match('/docs/search/')) { + return ( + + ); + } + + if (url.match('/docs/analytics/')) { + return ( + + ); + } + } + } + return ( + + ); +} + +URLIcon.propTypes = { + url: string, + style: object, + size: number, +}; diff --git a/src/components/common/search/URLIcon.module.css b/src/components/common/search/URLIcon.module.css new file mode 100644 index 000000000..f161cc40a --- /dev/null +++ b/src/components/common/search/URLIcon.module.css @@ -0,0 +1,4 @@ +.sectionItemIcon { + margin-bottom: 1rem; + object-fit: cover; +} diff --git a/src/components/common/search/getBreadcrumbText.js b/src/components/common/search/getBreadcrumbText.js new file mode 100644 index 000000000..5642b73b1 --- /dev/null +++ b/src/components/common/search/getBreadcrumbText.js @@ -0,0 +1,71 @@ +const matchComponents = url => { + let breadcrumbText = ''; + if (url.match('/list')) { + breadcrumbText = ' > List Components'; + } else if (url.match('/range')) { + breadcrumbText = ' > Range Components'; + } else if (url.match('/search')) { + breadcrumbText = ' > Search Components'; + } else if (url.match('/result')) { + breadcrumbText = ' > Result Components'; + } else if (url.match('/map')) { + breadcrumbText = ' > Map Components'; + } else if (url.match('/overview')) { + breadcrumbText = ' > Overview'; + } + return breadcrumbText; +}; + +// eslint-disable-next-line import/prefer-default-export +export function getBreadcrumbText(url) { + let breadcrumbText = ''; + try { + if (url.match('/docs/')) { + if (url.match('/docs/reactivesearch/')) { + if (url.match('/react/v3')) { + breadcrumbText += 'React V3'; + breadcrumbText += matchComponents(url); + } else if (url.match('/reactivesearch/vue/v1')) { + breadcrumbText += 'Vue V1'; + breadcrumbText += matchComponents(url); + } else if (url.match('/react/')) { + breadcrumbText += 'React'; + breadcrumbText += matchComponents(url); + } else if (url.match('/vue/')) { + breadcrumbText += 'Vue'; + breadcrumbText += matchComponents(url); + } + } else if (url.match('/docs/ai-search')) { + breadcrumbText += 'AI Search'; + } else if (url.match('/docs/data')) { + breadcrumbText += 'Managing Data'; + } else if (url.match('/docs/search/relevancy')) { + breadcrumbText += 'Search Relevancy'; + } else if (url.match('/docs/speed')) { + breadcrumbText += 'Speed'; + } else if (url.match('/docs/hosting')) { + breadcrumbText += 'Hosting'; + } else if (url.match('/docs/security')) { + breadcrumbText += 'Security'; + } else if (url.match('/docs/search/')) { + breadcrumbText += 'Search'; + } else if (url.match('/docs/analytics/')) { + breadcrumbText += 'Analytics'; + } else if (url.match('/docs/pipelines/')) { + breadcrumbText += 'Pipelines'; + } + } else if (url.match('https://www.reactivesearch.io/')) { + breadcrumbText = 'Website'; + } else if (url.match('https://blog.reactivesearch.io')) { + breadcrumbText = 'Blog'; + } + } catch { + console.error('Parsing error: Error forming breadcrumb'); + breadcrumbText = ''; + } finally { + if (!breadcrumbText) { + breadcrumbText = 'Reactivesearch'; + } + } + return breadcrumbText; +} diff --git a/src/components/common/search/index.js b/src/components/common/search/index.js deleted file mode 100644 index 31ea5aaaa..000000000 --- a/src/components/common/search/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as SearchModal } from './SearchModal'; diff --git a/src/components/common/transformRequest.js b/src/components/common/transformRequest.js new file mode 100644 index 000000000..ed1865ff0 --- /dev/null +++ b/src/components/common/transformRequest.js @@ -0,0 +1,37 @@ +import { SEARCH_COMPONENT_ID } from './constants'; + +/* + * Assumptions: + * 1. uses index `unified_reactivesearch` + * 2. Wants to filter only for documentation + */ + +// eslint-disable-next-line import/prefer-default-export +export const transformRequest = req => { + const body = JSON.parse(req.body); + + body.query = body.query.map(componentQuery => { + // handle when no search value is there. The component is making a suggestion query + if (componentQuery.id === SEARCH_COMPONENT_ID) { + return { + ...componentQuery, + showDistinctSuggestions: true, + react: { + and: ['term'], + }, + }; + } + return componentQuery; + }); + if (body.query) { + body.query.push({ + id: 'term', + type: 'term', + execute: false, + dataField: ['source.keyword'], + value: 'docs', + }); + } + const newReq = { ...req, body: JSON.stringify(body) }; + return newReq; +}; diff --git a/src/components/common/transformResponse.js b/src/components/common/transformResponse.js new file mode 100644 index 000000000..a2fdfab7b --- /dev/null +++ b/src/components/common/transformResponse.js @@ -0,0 +1,36 @@ +import { SEARCH_COMPONENT_ID } from './constants'; + +function removeHashFromURL(url) { + if (url) { + return url.replace(/#.*$/, ''); + } + return url; +} + +function filterDuplicatesByTitle(array) { + const uniqueValues = {}; + return array.filter(item => { + if (!uniqueValues[removeHashFromURL(item._source.url)]) { + uniqueValues[removeHashFromURL(item._source.url)] = 1; + return true; + } + if (uniqueValues[removeHashFromURL(item._source.url)] < 2) { + uniqueValues[removeHashFromURL(item._source.url)] += 1; + return true; + } + return false; + }); +} + +// eslint-disable-next-line +export async function transformResponse(res, componentId) { + if (componentId === SEARCH_COMPONENT_ID) { + const { hits } = res; + const filteredHits = filterDuplicatesByTitle(hits && hits.hits ? hits.hits : []); + return { + ...res, + hits: { hits: filteredHits }, + }; + } + return res; +} diff --git a/src/styles/prism.css b/src/styles/prism.css index 5f2eae5a3..56e2d7eac 100644 --- a/src/styles/prism.css +++ b/src/styles/prism.css @@ -304,11 +304,3 @@ pre > code { .gatsby-remark-prismjs-copy-button { margin-right: 15px; } - -body.dark .search-icon { - fill: #ffffff !important; -} - -body.light .search-icon { - fill: #082429 !important; -} \ No newline at end of file diff --git a/src/templates/markdown/post.js b/src/templates/markdown/post.js index b1c952389..09d7a3f9b 100644 --- a/src/templates/markdown/post.js +++ b/src/templates/markdown/post.js @@ -8,6 +8,7 @@ import { SidebarNav } from '../../components/common/sidebar'; import { PrevNextSection } from '../../components/common/prev-next'; import { Icon, TOC } from '../../components/common'; import { Helmet } from 'react-helmet'; +import { CREDENTIAL_FOR_DOCS } from '../../components/common/constants'; const getGitHubLink = absoluteFilePath => { const splitPath = absoluteFilePath.split('/content')[1]; @@ -26,6 +27,30 @@ class Post extends React.Component { this.toggleMobileMenu = this.toggleMobileMenu.bind(this); } + async componentDidMount() { + const { docId } = this.props && this.props.location && this.props.location.state; + if (docId) { + try { + await fetch( + 'https://appbase-demo-ansible-abxiydt-arc.searchbase.io/unified-reactivesearch-web-data/_analytics/document', + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${btoa(CREDENTIAL_FOR_DOCS)}`, + }, + body: JSON.stringify({ + document_id: docId, + user_id: 'test', + }), + }, + ); + } catch (e) { + console.error(`Couldn't index document suggestion.\n${e}`); + } + } + } + toggleMobileMenu() { this.setState(state => { return { isToggleOn: !state.isToggleOn }; @@ -87,13 +112,14 @@ class Post extends React.Component { - +