From 97230029f8a0ef512d7b84bfe85b8ff34cc12b73 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 24 Oct 2024 11:06:48 +0300
Subject: [PATCH 01/25] Create an Advanced Search Form layout
---
.../advanced-search-form.jsx | 175 ++++++++++++++++++
.../advanced-search-form.module.scss | 95 ++++++++++
.../advanced-search-form/bool-input.jsx | 10 +
.../advanced-search-form/choose-input.jsx | 14 ++
.../advanced-search-form/columns.jsx | 26 +++
.../advanced-search-form/interval-input.jsx | 23 +++
.../advanced-search-form/section.jsx | 11 ++
.../advanced-search-form/text-input.jsx | 12 ++
src/components/search-feed.jsx | 9 +-
9 files changed, 372 insertions(+), 3 deletions(-)
create mode 100644 src/components/advanced-search-form/advanced-search-form.jsx
create mode 100644 src/components/advanced-search-form/advanced-search-form.module.scss
create mode 100644 src/components/advanced-search-form/bool-input.jsx
create mode 100644 src/components/advanced-search-form/choose-input.jsx
create mode 100644 src/components/advanced-search-form/columns.jsx
create mode 100644 src/components/advanced-search-form/interval-input.jsx
create mode 100644 src/components/advanced-search-form/section.jsx
create mode 100644 src/components/advanced-search-form/text-input.jsx
diff --git a/src/components/advanced-search-form/advanced-search-form.jsx b/src/components/advanced-search-form/advanced-search-form.jsx
new file mode 100644
index 000000000..c16283da6
--- /dev/null
+++ b/src/components/advanced-search-form/advanced-search-form.jsx
@@ -0,0 +1,175 @@
+import { useEffect, useState } from 'react';
+import { useEvent } from 'react-use-event-hook';
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
+import { useMediaQuery } from '../hooks/media-query';
+import { ButtonLink } from '../button-link';
+import { Icon } from '../fontawesome-icons';
+import style from './advanced-search-form.module.scss';
+import { BoolInput } from './bool-input';
+import { ChooseInput } from './choose-input';
+import { Columns } from './columns';
+import { IntervalInput } from './interval-input';
+import { Section } from './section';
+import { TextInput } from './text-input';
+
+export function AdvancedSearchForm() {
+ const isWideScreen = useMediaQuery('(min-width: 768px)');
+ const [formExpanded, setFormExpanded] = useState(false);
+ const [docsExpanded, setDocsExpanded] = useState(false);
+
+ const expandForm = useEvent(() => setFormExpanded(true));
+ const expandDocs = useEvent(() => setDocsExpanded(true));
+
+ useEffect(() => {
+ if (isWideScreen) {
+ setFormExpanded(false);
+ setDocsExpanded(false);
+ }
+ }, [isWideScreen]);
+
+ const showFullForm = isWideScreen || formExpanded;
+ const showFullDocs = isWideScreen || docsExpanded;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {showFullForm ? (
+ <>
+
+ Any
+ Public
+ Protected
+ Private
+
+
+ With or without
+ Images
+ Audio
+ Any files
+
+
+
+
+
+
+ >
+ ) : null}
+
+
+ {showFullForm ? (
+
+
+
+
+
+
+
+
+ Don’t exclude
+ Images
+ Audio
+ Any files
+
+
+ Any
+ Public
+ Protected
+ Private
+
+
+
+ ) : null}
+ {!showFullForm ? (
+
+
+ Show all conditions
+
+
+ ) : null}
+
+
+ Search query: foo bar
+
+ {showFullDocs ? (
+ <>
+ {' '}
+
+ Use double-quotes to search words in the exact form and specific word order:{' '}
+ "freefeed version"
+
+ Use the asterisk symbol (*
) to search word by prefix: free* . The
+ minimum prefix length is two characters.
+
+ Use the pipe symbol (|
) between words to search any of them:{' '}
+ freefeed | version
+
+ Use the minus sign (-
) to exclude some word from search results:{' '}
+ freefeed -version
+
+ Use the plus sign (+
) to specify word order: freefeed + version
+
+
+
+ Learn the{' '}
+
+ full query syntax
+ {' '}
+ for more advanced search requests.
+
+ >
+ ) : (
+ <>
+
+ Use double-quotes to search words in the exact form and specific word order:{' '}
+ "freefeed version"
+
+ Use the asterisk symbol (*
) to search word by prefix: free* . The
+ minimum prefix length is two characters.
+
+
+
+ Show search query syntax
+ help
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/advanced-search-form/advanced-search-form.module.scss b/src/components/advanced-search-form/advanced-search-form.module.scss
new file mode 100644
index 000000000..496cf8e73
--- /dev/null
+++ b/src/components/advanced-search-form/advanced-search-form.module.scss
@@ -0,0 +1,95 @@
+.form {
+ label {
+ margin-bottom: 0;
+ font-weight: normal;
+ }
+
+ :global(.checkbox) {
+ margin: 0;
+ }
+}
+
+.section {
+ margin-bottom: 1.5em;
+}
+
+.sectionSticky {
+ @media (max-width: 767px) {
+ position: sticky;
+ bottom: 0;
+ padding: 0.5em 0;
+ margin: -0.5em 0 1em;
+ background: white; // TODO: dark mode
+ }
+}
+
+.sectionTitle {
+ border-bottom: none;
+ font-size: 1.2em;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+}
+
+.searchScopes {
+ margin-top: 0.5em;
+ display: flex;
+
+ label {
+ margin-left: 0.6em;
+ display: inline-flex;
+ gap: 0.4em;
+ cursor: pointer;
+ }
+}
+
+.columns {
+ display: flex;
+ gap: 0.4em;
+ flex-direction: column;
+}
+
+@media (min-width: 768px) {
+ .columns {
+ display: grid;
+ gap: 0.2em 2em;
+ grid-auto-flow: column;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(var(--n-rows), 1fr);
+ }
+}
+
+.inputRow {
+ display: flex;
+ gap: 0 0.5em;
+ flex-direction: column;
+
+ @media (min-width: 380px) {
+ flex-direction: row;
+ align-items: center;
+
+ & > *:first-child {
+ flex: 0 0 50%;
+ }
+
+ & > *:last-child {
+ flex: 1;
+ }
+ }
+}
+
+.intervalBox {
+ display: flex;
+ gap: 0.25em;
+
+ & > *:first-child {
+ flex: 0 0 50%;
+ }
+
+ & > *:last-child {
+ flex: 1;
+ }
+}
+
+.expandIcon {
+ opacity: 0.6;
+}
diff --git a/src/components/advanced-search-form/bool-input.jsx b/src/components/advanced-search-form/bool-input.jsx
new file mode 100644
index 000000000..a912b3d0d
--- /dev/null
+++ b/src/components/advanced-search-form/bool-input.jsx
@@ -0,0 +1,10 @@
+export function BoolInput({ label }) {
+ return (
+
+
+
+ {label}
+
+
+ );
+}
diff --git a/src/components/advanced-search-form/choose-input.jsx b/src/components/advanced-search-form/choose-input.jsx
new file mode 100644
index 000000000..af3d50b71
--- /dev/null
+++ b/src/components/advanced-search-form/choose-input.jsx
@@ -0,0 +1,14 @@
+import { useId } from 'react';
+import style from './advanced-search-form.module.scss';
+
+export function ChooseInput({ label, children: options }) {
+ const id = useId();
+ return (
+
+ {label}
+
+ {options}
+
+
+ );
+}
diff --git a/src/components/advanced-search-form/columns.jsx b/src/components/advanced-search-form/columns.jsx
new file mode 100644
index 000000000..deabaf5fb
--- /dev/null
+++ b/src/components/advanced-search-form/columns.jsx
@@ -0,0 +1,26 @@
+import { Children } from 'react';
+import style from './advanced-search-form.module.scss';
+
+export function Columns({ children }) {
+ const nRows = Math.ceil(countElements(children) / 2);
+ return (
+
+ {children}
+
+ );
+}
+
+function countElements(children) {
+ let n = 0;
+ Children.forEach(children, (child) => {
+ if (!child) {
+ return;
+ }
+ if (child.type === Symbol.for('react.fragment')) {
+ n += countElements(child.props.children);
+ } else {
+ n++;
+ }
+ });
+ return n;
+}
diff --git a/src/components/advanced-search-form/interval-input.jsx b/src/components/advanced-search-form/interval-input.jsx
new file mode 100644
index 000000000..a5feacfd1
--- /dev/null
+++ b/src/components/advanced-search-form/interval-input.jsx
@@ -0,0 +1,23 @@
+import { useId } from 'react';
+import style from './advanced-search-form.module.scss';
+
+export function IntervalInput({ label }) {
+ const id = useId();
+ return (
+
+
{label}
+
+
+ <
+ <=
+ =
+
+ >=
+
+ >
+
+
+
+
+ );
+}
diff --git a/src/components/advanced-search-form/section.jsx b/src/components/advanced-search-form/section.jsx
new file mode 100644
index 000000000..8a43e24cb
--- /dev/null
+++ b/src/components/advanced-search-form/section.jsx
@@ -0,0 +1,11 @@
+import cn from 'classnames';
+import style from './advanced-search-form.module.scss';
+
+export function Section({ title, children, sticky = false }) {
+ return (
+
+ {title ? {title} : null}
+ {children}
+
+ );
+}
diff --git a/src/components/advanced-search-form/text-input.jsx b/src/components/advanced-search-form/text-input.jsx
new file mode 100644
index 000000000..b86a872e4
--- /dev/null
+++ b/src/components/advanced-search-form/text-input.jsx
@@ -0,0 +1,12 @@
+import { useId } from 'react';
+import style from './advanced-search-form.module.scss';
+
+export function TextInput({ label, placeholder = '', type = 'text' }) {
+ const id = useId();
+ return (
+
+ {label}
+
+
+ );
+}
diff --git a/src/components/search-feed.jsx b/src/components/search-feed.jsx
index 439247ade..7aa4de5a5 100644
--- a/src/components/search-feed.jsx
+++ b/src/components/search-feed.jsx
@@ -12,8 +12,11 @@ import { ButtonLink } from './button-link';
import { Icon } from './fontawesome-icons';
import { SignInLink } from './sign-in-link';
-const SearchFormAdvanced = lazyComponent(
- () => import('./search-form-advanced').then((m) => ({ default: m.SearchFormAdvanced })),
+const AdvancedSearchForm = lazyComponent(
+ () =>
+ import('./advanced-search-form/advanced-search-form').then((m) => ({
+ default: m.AdvancedSearchForm,
+ })),
{
fallback: Loading form...
,
errorMessage: "Couldn't load search form",
@@ -68,7 +71,7 @@ function FeedHandler(props) {
>
)}
- {(!queryString || advFormVisible) && }
+ {(!queryString || advFormVisible) && }
{props.entries.length > 0 && }
{props.entries.length > 0 && (
From 0c9445bfd2a2185c7f1f54299ee431e287cee4b5 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 24 Oct 2024 18:42:31 +0300
Subject: [PATCH 02/25] Parse and build search query
---
.../advanced-search-form.jsx | 352 +++++++++++-------
.../advanced-search-form/bool-input.jsx | 17 +-
.../advanced-search-form/choose-input.jsx | 23 +-
.../advanced-search-form/context.js | 3 +
.../advanced-search-form/helpers.js | 121 ++++++
.../advanced-search-form/interval-input.jsx | 45 ++-
.../advanced-search-form/reducer.js | 23 ++
.../advanced-search-form/text-input.jsx | 25 +-
8 files changed, 462 insertions(+), 147 deletions(-)
create mode 100644 src/components/advanced-search-form/context.js
create mode 100644 src/components/advanced-search-form/helpers.js
create mode 100644 src/components/advanced-search-form/reducer.js
diff --git a/src/components/advanced-search-form/advanced-search-form.jsx b/src/components/advanced-search-form/advanced-search-form.jsx
index c16283da6..315e5e653 100644
--- a/src/components/advanced-search-form/advanced-search-form.jsx
+++ b/src/components/advanced-search-form/advanced-search-form.jsx
@@ -1,9 +1,11 @@
-import { useEffect, useState } from 'react';
+import { browserHistory } from 'react-router';
+import { useEffect, useMemo, useReducer, useState } from 'react';
import { useEvent } from 'react-use-event-hook';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
import { useMediaQuery } from '../hooks/media-query';
import { ButtonLink } from '../button-link';
import { Icon } from '../fontawesome-icons';
+import { useSearchQuery } from '../hooks/search-query';
import style from './advanced-search-form.module.scss';
import { BoolInput } from './bool-input';
import { ChooseInput } from './choose-input';
@@ -11,6 +13,16 @@ import { Columns } from './columns';
import { IntervalInput } from './interval-input';
import { Section } from './section';
import { TextInput } from './text-input';
+import {
+ intervalFilters,
+ parseQuery,
+ useCheckboxField,
+ usernameFilters,
+ usernames,
+ useTextField,
+} from './helpers';
+import { reducer } from './reducer';
+import { filtersContext } from './context';
export function AdvancedSearchForm() {
const isWideScreen = useMediaQuery('(min-width: 768px)');
@@ -30,146 +42,222 @@ export function AdvancedSearchForm() {
const showFullForm = isWideScreen || formExpanded;
const showFullDocs = isWideScreen || docsExpanded;
+ const initialQuery = parseQuery(useSearchQuery());
+
+ const [query, queryAttrs] = useTextField(initialQuery.text);
+ const [inPosts, inPostsAttrs] = useCheckboxField(initialQuery.inPosts);
+ const [inComments, inCommentsAttrs] = useCheckboxField(initialQuery.inComments);
+
+ const [filters, dispatch] = useReducer(reducer, initialQuery.filters);
+ const ctxValue = useMemo(() => [filters, dispatch], [filters, dispatch]);
+
+ const resultingQuery = useMemo(() => {
+ if (!inPosts && !inComments) {
+ return '';
+ }
+ return [
+ inPosts && !inComments ? 'in-body:' : '',
+ inComments && !inPosts ? 'in-comments:' : '',
+ query.trim(),
+ ...Object.entries(filters).map(([k, v]) => {
+ if (intervalFilters.has(k) && !/\d/.test(v)) {
+ return null;
+ }
+ if (usernameFilters.has(k)) {
+ v = usernames(v);
+ }
+ return k + v;
+ }),
+ ]
+ .filter(Boolean)
+ .join(' ');
+ }, [inPosts, inComments, query, filters]);
+
+ const onSearch = useEvent(() =>
+ browserHistory.push(`/search?q=${encodeURIComponent(resultingQuery)}`),
+ );
+
return (
-
-
-
-
-
-
-
-
-
-
-
- {showFullForm ? (
- <>
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {showFullForm ? (
+ <>
+
+ Any
+ Public
+ Protected
+ Private
+
+
+ With or without
+ Images
+ Audio
+ Any files
+
+
+
+
+
+
+ >
+ ) : null}
+
+
+ {showFullForm ? (
+
+
+
+
+
+
+ Don’t exclude
+ Images
+ Audio
+ Any files
+
+
Any
Public
Protected
Private
-
- With or without
- Images
- Audio
- Any files
-
-
-
-
-
-
- >
- ) : null}
-
-
- {showFullForm ? (
-
-
-
-
-
-
-
-
- Don’t exclude
- Images
- Audio
- Any files
-
-
- Any
- Public
- Protected
- Private
-
-
-
- ) : null}
- {!showFullForm ? (
-
-
- Show all conditions
-
-
- ) : null}
-
-
- Search query: foo bar
-
- {showFullDocs ? (
- <>
- {' '}
-
- Use double-quotes to search words in the exact form and specific word order:{' '}
- "freefeed version"
-
- Use the asterisk symbol (*
) to search word by prefix: free* . The
- minimum prefix length is two characters.
-
- Use the pipe symbol (|
) between words to search any of them:{' '}
- freefeed | version
-
- Use the minus sign (-
) to exclude some word from search results:{' '}
- freefeed -version
-
- Use the plus sign (+
) to specify word order: freefeed + version
-
-
-
- Learn the{' '}
-
- full query syntax
- {' '}
- for more advanced search requests.
-
- >
- ) : (
- <>
+
+
+ ) : null}
+ {!showFullForm ? (
- Use double-quotes to search words in the exact form and specific word order:{' '}
- "freefeed version"
-
- Use the asterisk symbol (*
) to search word by prefix: free* . The
- minimum prefix length is two characters.
+
+ Show all conditions
+
+ ) : null}
+
+ {resultingQuery ? (
-
- Show search query syntax
- help
-
+ Search query: {resultingQuery}
- >
- )}
-
+ ) : null}
+ {showFullDocs ? (
+ <>
+ {' '}
+
+ Use double-quotes to search words in the exact form and specific word order:{' '}
+ "freefeed version"
+
+ Use the asterisk symbol (*
) to search word by prefix: free* . The
+ minimum prefix length is two characters.
+
+ Use the pipe symbol (|
) between words to search any of them:{' '}
+ freefeed | version
+
+ Use the minus sign (-
) to exclude some word from search results:{' '}
+ freefeed -version
+
+ Use the plus sign (+
) to specify word order: freefeed + version
+
+
+
+ Learn the{' '}
+
+ full query syntax
+ {' '}
+ for more advanced search requests.
+
+ >
+ ) : (
+ <>
+
+ Use double-quotes to search words in the exact form and specific word order:{' '}
+ "freefeed version"
+
+ Use the asterisk symbol (*
) to search word by prefix: free* . The
+ minimum prefix length is two characters.
+
+
+
+ Show search query syntax
+ help
+
+
+ >
+ )}
+
+
);
}
diff --git a/src/components/advanced-search-form/bool-input.jsx b/src/components/advanced-search-form/bool-input.jsx
index a912b3d0d..fe0cb5204 100644
--- a/src/components/advanced-search-form/bool-input.jsx
+++ b/src/components/advanced-search-form/bool-input.jsx
@@ -1,8 +1,21 @@
-export function BoolInput({ label }) {
+import { useEvent } from 'react-use-event-hook';
+import { useContext } from 'react';
+import { removeFilter, setFilter } from './reducer';
+import { filtersContext } from './context';
+
+export function BoolInput({ label, filter }) {
+ const [filters, dispatch] = useContext(filtersContext);
+ const onChange = useEvent((e) => {
+ if (e.target.checked) {
+ dispatch(setFilter(filter, ''));
+ } else {
+ dispatch(removeFilter(filter));
+ }
+ });
return (
-
+
{label}
diff --git a/src/components/advanced-search-form/choose-input.jsx b/src/components/advanced-search-form/choose-input.jsx
index af3d50b71..6a24b0374 100644
--- a/src/components/advanced-search-form/choose-input.jsx
+++ b/src/components/advanced-search-form/choose-input.jsx
@@ -1,12 +1,29 @@
-import { useId } from 'react';
+import { useContext, useId } from 'react';
+import { useEvent } from 'react-use-event-hook';
import style from './advanced-search-form.module.scss';
+import { filtersContext } from './context';
+import { removeFilter, setFilter } from './reducer';
-export function ChooseInput({ label, children: options }) {
+export function ChooseInput({ label, children: options, filter }) {
const id = useId();
+ const [filters, dispatch] = useContext(filtersContext);
+ const onChange = useEvent((e) => {
+ const v = e.target.value;
+ if (v.trim() !== '') {
+ dispatch(setFilter(filter, v));
+ } else {
+ dispatch(removeFilter(filter));
+ }
+ });
return (
{label}
-
+
{options}
diff --git a/src/components/advanced-search-form/context.js b/src/components/advanced-search-form/context.js
new file mode 100644
index 000000000..8e5680b8a
--- /dev/null
+++ b/src/components/advanced-search-form/context.js
@@ -0,0 +1,3 @@
+import { createContext } from 'react';
+
+export const filtersContext = createContext();
diff --git a/src/components/advanced-search-form/helpers.js b/src/components/advanced-search-form/helpers.js
new file mode 100644
index 000000000..f7ff8611c
--- /dev/null
+++ b/src/components/advanced-search-form/helpers.js
@@ -0,0 +1,121 @@
+import { useState } from 'react';
+
+export function useTextField(initialValue) {
+ const [value, setValue] = useState(initialValue);
+ const attrs = {
+ value,
+ onChange: (e) => setValue(e.target.value),
+ };
+ return [value, attrs];
+}
+
+export function useCheckboxField(initialValue) {
+ const [value, setValue] = useState(initialValue);
+ const attrs = {
+ checked: value,
+ onChange: (e) => setValue(e.target.checked),
+ };
+ return [value, attrs];
+}
+
+export const supportedFilters = [
+ 'in-my:friends',
+ 'in-my:discussions',
+ 'from:me',
+ 'in-my:messages',
+ 'in-my:saves',
+ 'by:me',
+ 'by:',
+ 'from:',
+ 'commented-by:',
+ 'post-date:>=',
+ 'post-date:<',
+ 'in:',
+ 'is:',
+ 'has:',
+ 'comments:',
+ 'likes:',
+ 'clikes:',
+ 'commented-by:',
+ 'cliked-by:',
+ '-from:',
+ '-in:',
+ '-commented-by:',
+ '-has:',
+ '-is:',
+];
+
+export const intervalFilters = new Set(['comments:', 'likes:', 'clikes:']);
+export const usernameFilters = new Set([
+ 'by:',
+ 'from:',
+ 'commented-by:',
+ 'in:',
+ '-in:',
+ '-from:',
+ '-commented-by:',
+]);
+
+export function usernames(text = '') {
+ text = text.toLowerCase();
+ const re = /[a-z\d-]+/g;
+ let m;
+ const result = [];
+ while ((m = re.exec(text))) {
+ result.push(m[0]);
+ }
+ return result.join(',');
+}
+
+/**
+ * Parses a query string into a query text and an array of conditions
+ *
+ * @param {string} query
+ */
+export function parseQuery(query) {
+ const result = {
+ inPosts: true,
+ inComments: true,
+ text: '',
+ filters: {},
+ };
+ const parts = query.split(/\s+/);
+ if (parts.length === 0) {
+ return result;
+ }
+
+ if (parts[0] === 'in-body:') {
+ result.inComments = false;
+ parts.shift();
+ } else if (parts[0] === 'in-comments:') {
+ result.inPosts = false;
+ parts.shift();
+ }
+
+ const textParts = [];
+
+ loop: for (const rawPart of parts) {
+ const part = rawPart;
+ for (const filter of supportedFilters) {
+ if (part === filter) {
+ if (filter in result.filters) {
+ textParts.push(rawPart);
+ } else {
+ result.filters[filter] = '';
+ }
+ continue loop;
+ }
+ if (part.startsWith(filter)) {
+ if (filter in result.filters) {
+ textParts.push(rawPart);
+ } else {
+ result.filters[filter] = part.slice(filter.length);
+ }
+ continue loop;
+ }
+ }
+ textParts.push(rawPart);
+ }
+ result.text = textParts.join(' ');
+ return result;
+}
diff --git a/src/components/advanced-search-form/interval-input.jsx b/src/components/advanced-search-form/interval-input.jsx
index a5feacfd1..1747fac87 100644
--- a/src/components/advanced-search-form/interval-input.jsx
+++ b/src/components/advanced-search-form/interval-input.jsx
@@ -1,22 +1,53 @@
-import { useId } from 'react';
+import { useContext, useId } from 'react';
+import { useEvent } from 'react-use-event-hook';
import style from './advanced-search-form.module.scss';
+import { filtersContext } from './context';
+import { setFilter } from './reducer';
-export function IntervalInput({ label }) {
+export function IntervalInput({ label, filter }) {
const id = useId();
+ const [filters, dispatch] = useContext(filtersContext);
+
+ const value = filter in filters ? filters[filter] : '>=';
+ const m = /^(<=?|>=?|=)?(\d+)?$/.exec(value);
+ let op = '>=';
+ let num = '';
+ if (m) {
+ op = m[1] || '=';
+ // eslint-disable-next-line prefer-destructuring
+ num = m[2] || '';
+ }
+
+ const onOpChange = useEvent((e) => {
+ const v = e.target.value;
+ dispatch(setFilter(filter, v + num));
+ });
+
+ const onNumChange = useEvent((e) => {
+ const v = e.target.value;
+ dispatch(setFilter(filter, op + v));
+ });
+
return (
);
diff --git a/src/components/advanced-search-form/reducer.js b/src/components/advanced-search-form/reducer.js
new file mode 100644
index 000000000..10a0f3396
--- /dev/null
+++ b/src/components/advanced-search-form/reducer.js
@@ -0,0 +1,23 @@
+import { omit } from 'lodash-es';
+
+const SET = 'SET';
+const REMOVE = 'REMOVE';
+
+export function reducer(state, action) {
+ switch (action.type) {
+ case SET:
+ return { ...state, [action.filter]: action.value };
+ case REMOVE:
+ return omit(state, action.filter);
+ default:
+ return state;
+ }
+}
+
+export function setFilter(filter, value) {
+ return { type: SET, filter, value };
+}
+
+export function removeFilter(filter) {
+ return { type: REMOVE, filter };
+}
diff --git a/src/components/advanced-search-form/text-input.jsx b/src/components/advanced-search-form/text-input.jsx
index b86a872e4..c5ab2193d 100644
--- a/src/components/advanced-search-form/text-input.jsx
+++ b/src/components/advanced-search-form/text-input.jsx
@@ -1,12 +1,31 @@
-import { useId } from 'react';
+import { useContext, useId } from 'react';
+import { useEvent } from 'react-use-event-hook';
import style from './advanced-search-form.module.scss';
+import { filtersContext } from './context';
+import { removeFilter, setFilter } from './reducer';
-export function TextInput({ label, placeholder = '', type = 'text' }) {
+export function TextInput({ label, placeholder = '', type = 'text', filter }) {
const id = useId();
+ const [filters, dispatch] = useContext(filtersContext);
+ const onChange = useEvent((e) => {
+ const v = e.target.value;
+ if (v.trim() !== '') {
+ dispatch(setFilter(filter, v));
+ } else {
+ dispatch(removeFilter(filter));
+ }
+ });
return (
{label}
-
+
);
}
From 1f75f8e68e99130121f5a9e71cab518d006b07d0 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Fri, 25 Oct 2024 12:44:10 +0300
Subject: [PATCH 03/25] Fix the autocomplete pattern search logic for the
zero-width pattern case
---
src/components/autocomplete/autocomplete.jsx | 27 +++++++-------------
1 file changed, 9 insertions(+), 18 deletions(-)
diff --git a/src/components/autocomplete/autocomplete.jsx b/src/components/autocomplete/autocomplete.jsx
index aeb5197ae..b083478a7 100644
--- a/src/components/autocomplete/autocomplete.jsx
+++ b/src/components/autocomplete/autocomplete.jsx
@@ -105,31 +105,22 @@ export function Autocomplete({ inputRef, context, anchor = defaultAnchor }) {
* @returns {[number, number]|null}
*/
function getQueryPosition({ value, selectionStart }, anchor) {
- if (!selectionStart) {
- return null;
- }
-
anchor.lastIndex = 0;
-
- let found = -1;
while (anchor.exec(value) !== null) {
- if (anchor.lastIndex > selectionStart) {
+ const pos = anchor.lastIndex;
+ if (pos > selectionStart) {
break;
}
- found = anchor.lastIndex;
- }
- if (found === -1) {
- return null;
- }
-
- const match = value.slice(found).match(/^[a-z\d-]+/i)?.[0];
- // Check that the caret is inside the match or is at its edge
- if (!match || match.length <= selectionStart - found - 1) {
- return null;
+ const match = value.slice(pos).match(/^[a-z\d-]+/i)?.[0];
+ // Check that the caret is inside the match or is at its edge
+ if (match && match.length > selectionStart - pos - 1) {
+ return [pos, pos + match.length];
+ }
+ anchor.lastIndex = pos + 1;
}
- return [found, found + match.length];
+ return null;
}
/**
From c0045850f85eaf02b8e82045adbadaaaa03aa84f Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Fri, 25 Oct 2024 12:52:04 +0300
Subject: [PATCH 04/25] Add autocomplete and make inactive options gray
---
.../advanced-search-form.jsx | 8 +---
.../advanced-search-form.module.scss | 44 ++++++++++++++++++-
.../advanced-search-form/choose-input.jsx | 6 ++-
.../advanced-search-form/text-input.jsx | 34 ++++++++++----
4 files changed, 74 insertions(+), 18 deletions(-)
diff --git a/src/components/advanced-search-form/advanced-search-form.jsx b/src/components/advanced-search-form/advanced-search-form.jsx
index 315e5e653..0f99c7fd7 100644
--- a/src/components/advanced-search-form/advanced-search-form.jsx
+++ b/src/components/advanced-search-form/advanced-search-form.jsx
@@ -123,7 +123,7 @@ export function AdvancedSearchForm() {
/>
-
+
{showFullForm ? (
<>
@@ -163,11 +163,7 @@ export function AdvancedSearchForm() {
placeholder="user1, user2"
filter="-from:"
/>
-
+
{label}
{options}
diff --git a/src/components/advanced-search-form/text-input.jsx b/src/components/advanced-search-form/text-input.jsx
index c5ab2193d..d65ed75d8 100644
--- a/src/components/advanced-search-form/text-input.jsx
+++ b/src/components/advanced-search-form/text-input.jsx
@@ -1,8 +1,13 @@
-import { useContext, useId } from 'react';
+import cn from 'classnames';
+import { useContext, useId, useRef } from 'react';
import { useEvent } from 'react-use-event-hook';
+import { Autocomplete } from '../autocomplete/autocomplete';
import style from './advanced-search-form.module.scss';
import { filtersContext } from './context';
import { removeFilter, setFilter } from './reducer';
+import { usernameFilters } from './helpers';
+
+const autocompleteAnchor = /^|[^a-z\d]/gi;
export function TextInput({ label, placeholder = '', type = 'text', filter }) {
const id = useId();
@@ -15,17 +20,28 @@ export function TextInput({ label, placeholder = '', type = 'text', filter }) {
dispatch(removeFilter(filter));
}
});
+ const value = filter in filters ? filters[filter] : '';
+ const input = useRef();
+ const withAutocomplete = usernameFilters.has(filter);
return (
);
}
From a3a09fa235f976804d47b77c19e1c6b0a81b8593 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Fri, 25 Oct 2024 13:03:05 +0300
Subject: [PATCH 05/25] Adapt styles for dark mode
---
.../advanced-search-form.module.scss | 23 +++++++++++++++----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/src/components/advanced-search-form/advanced-search-form.module.scss b/src/components/advanced-search-form/advanced-search-form.module.scss
index ab6e4ab1b..196239a77 100644
--- a/src/components/advanced-search-form/advanced-search-form.module.scss
+++ b/src/components/advanced-search-form/advanced-search-form.module.scss
@@ -1,6 +1,19 @@
+@import '../../../styles/helvetica/dark-vars.scss';
+
.form {
--text-color: #333;
--placeholder-color: #999;
+ --background-color: #fff;
+
+ :global(.dark-theme) & {
+ --text-color: #{$text-color};
+ --placeholder-color: #777;
+ --background-color: #{$bg-color};
+ }
+
+ input::placeholder {
+ color: var(--placeholder-color);
+ }
label {
margin-bottom: 0;
@@ -22,7 +35,7 @@
bottom: 0;
padding: 0.5em 0;
margin: -0.5em 0 1em;
- background: white; // TODO: dark mode
+ background: var(--background-color);
}
}
@@ -113,7 +126,7 @@
}
.selector {
- color: var(--placeholder-color);
+ color: var(--placeholder-color) !important;
option[value=''] {
color: var(--placeholder-color);
@@ -125,13 +138,13 @@
}
.selectorChosen {
- color: var(--text-color);
+ color: var(--text-color) !important;
}
.textInput {
- color: var(--placeholder-color);
+ color: var(--placeholder-color) !important;
}
.textInputChosen {
- color: var(--text-color);
+ color: var(--text-color) !important;
}
From dd23267cc0046836fd90e59dc5c0b7b2e1e92034 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Fri, 25 Oct 2024 13:09:44 +0300
Subject: [PATCH 06/25] Remove previous advanced search form
---
src/components/search-form-advanced.jsx | 307 ------------------
.../search-form-advanced.module.scss | 19 --
2 files changed, 326 deletions(-)
delete mode 100644 src/components/search-form-advanced.jsx
delete mode 100644 src/components/search-form-advanced.module.scss
diff --git a/src/components/search-form-advanced.jsx b/src/components/search-form-advanced.jsx
deleted file mode 100644
index edd06c059..000000000
--- a/src/components/search-form-advanced.jsx
+++ /dev/null
@@ -1,307 +0,0 @@
-/* global CONFIG */
-import { useCallback, useMemo, useEffect } from 'react';
-import { useField, useForm } from 'react-final-form-hooks';
-import { useSelector } from 'react-redux';
-import { withRouter } from 'react-router';
-
-import { CheckboxInput, RadioInput } from './form-utils';
-import styles from './search-form-advanced.module.scss';
-import { useSearchQuery } from './hooks/search-query';
-
-const SCOPE_ALL = 'all';
-const SCOPE_BODY = 'body';
-const SCOPE_COMMENTS = 'comments';
-
-export const SearchFormAdvanced = withRouter(function SearchFormAdvanced({ router }) {
- const isAuthorized = useSelector((state) => !!state.user.id);
- const queryString = useSearchQuery();
-
- const form = useForm(
- useMemo(
- () => ({
- initialValues: parseQuery(queryString),
- onSubmit: (values) => {
- const q = generateQuery(values);
- q && router.push(`/search?q=${encodeURIComponent(q)}`);
- },
- }),
- [router, queryString],
- ),
- );
-
- const query = useField('query', form.form);
- const inBody = useField('inBody', form.form);
- const inComments = useField('inComments', form.form);
- const postsFilter = useField('filter', form.form);
- const inMy = useField('inMy', form.form);
- const inFeeds = useField('in', form.form);
- const fromUsers = useField('from', form.form);
- const commentedBy = useField('commentedBy', form.form);
- const likedBy = useField('likedBy', form.form);
-
- const scope = useMemo(() => {
- if (inBody.input.value && !inComments.input.value) {
- return SCOPE_BODY;
- } else if (!inBody.input.value && inComments.input.value) {
- return SCOPE_COMMENTS;
- }
- return SCOPE_ALL;
- }, [inBody.input.value, inComments.input.value]);
-
- // author:me isnt available in in-body: scope, so selecting from:me
- useEffect(() => {
- if (scope === SCOPE_BODY && postsFilter.input.value === 'author:me') {
- postsFilter.input.onChange('from:me');
- }
- }, [scope, postsFilter.input]);
-
- const txt = useCallback(
- (inAllText, inBodyText, inCommentsText) => {
- if (scope === SCOPE_BODY) {
- return inBodyText;
- } else if (scope === SCOPE_COMMENTS) {
- return inCommentsText;
- }
- return inAllText;
- },
- [scope],
- );
-
- const resultingQuery = generateQuery(form.values);
-
- return (
-
-
- {resultingQuery && (
-
- Search query: {resultingQuery}
-
- )}
-
- Use double-quotes to search words in the exact form and specific word order:{' '}
- "freefeed version"
-
- Use the asterisk symbol (*
) to search word by prefix: free* . The
- minimum prefix length is two characters.
-
- Use the pipe symbol (|
) between words to search any of them:{' '}
- freefeed | version
-
- Use the minus sign (-
) to exclude some word from search results:{' '}
- freefeed -version
-
- Use the plus sign (+
) to specify word order: freefeed + version
-
-
-
- Learn the{' '}
-
- full query syntax
- {' '}
- for more advanced search requests.
-
-
- );
-});
-
-function usernames(text = '') {
- text = text.toLowerCase();
- const re = /[a-z\d-]+/g;
- let m;
- const result = [];
- while ((m = re.exec(text))) {
- result.push(m[0]);
- }
- return result.join(',');
-}
-
-function generateQuery(values) {
- const scope =
- values.inBody && !values.inComments
- ? SCOPE_BODY
- : !values.inBody && values.inComments
- ? SCOPE_COMMENTS
- : SCOPE_ALL;
-
- const parts = [];
- scope === SCOPE_BODY && parts.push('in-body:');
- scope === SCOPE_COMMENTS && parts.push('in-comments:');
- const { filter } = values;
- switch (filter) {
- case 'in-my:friends':
- case 'from:me':
- case 'author:me':
- parts.push(filter);
- break;
- case 'in-my:':
- parts.push(filter + values.inMy);
- break;
- case 'in:': {
- const v = usernames(values.in);
- v && parts.push(filter + v);
- break;
- }
- case 'from:': {
- const v = usernames(values.from);
- v && parts.push(filter + v);
- break;
- }
- case 'commented-by:': {
- const v = usernames(values.commentedBy);
- v && parts.push(filter + v);
- break;
- }
- case 'liked-by:': {
- const v = usernames(values.likedBy);
- v && parts.push(filter + v);
- break;
- }
- }
- parts.push(values.query.trim());
- return parts.filter(Boolean).join(' ');
-}
-
-function parseQuery(query) {
- const values = {
- query: query.trim(),
- inBody: true,
- inComments: true,
- filter: '',
- inMy: 'discussions',
- inFeeds: '',
- from: '',
- commentedBy: '',
- likedBy: '',
- };
-
- if (values.query === '') {
- return values;
- }
-
- const trimFirstWord = () => (values.query = values.query.replace(/^\S+\s+/, ''));
-
- if (/^in-body:\s+/.test(values.query)) {
- values.inComments = false;
- trimFirstWord();
- } else if (/^in-comments:\s+/.test(values.query)) {
- values.inBody = false;
- trimFirstWord();
- }
-
- let m = /^(in-my:friends|from:me|author:me)\b/.exec(values.query);
- if (m) {
- values.filter = m[1]; // eslint-disable-line prefer-destructuring
- trimFirstWord();
- return values;
- }
-
- m = /^(in-my:)(discussions|saves|directs)\b/.exec(values.query);
- if (m) {
- [, values.filter, values.inMy] = m;
- trimFirstWord();
- return values;
- }
-
- m = /^(in:|from:|commented-by:|liked-by:)(\S+)\s+/.exec(values.query);
- if (m) {
- values.filter = m[1]; // eslint-disable-line prefer-destructuring
- values[kebabToCamel(m[1].replace(':', ''))] = m[2].replace(',', ', ');
- trimFirstWord();
- return values;
- }
-
- return values;
-}
-
-function kebabToCamel(text) {
- return text
- .split('-')
- .map((w, i) => (i ? w.charAt(0).toUpperCase() + w.slice(1) : w))
- .join('');
-}
diff --git a/src/components/search-form-advanced.module.scss b/src/components/search-form-advanced.module.scss
deleted file mode 100644
index 27cd57713..000000000
--- a/src/components/search-form-advanced.module.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-.queryInputGroup {
- margin-bottom: 0.5em;
-}
-
-.options {
- margin-bottom: 1em;
-
- label {
- font-weight: normal;
-
- &.inline {
- margin-right: 1em;
- }
- }
-
- :global(.input-group) {
- min-height: 2.5em;
- }
-}
From 1f44651203717836d50da811eedfaaa93cfd6a21 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Fri, 25 Oct 2024 16:44:57 +0300
Subject: [PATCH 07/25] Add "Advanced search options" dropdown
---
src/components/layout-header.jsx | 19 ++++++++-
src/components/layout-header.module.scss | 52 ++++++++++++++++++++++++
src/components/search-feed.jsx | 16 +++++---
3 files changed, 81 insertions(+), 6 deletions(-)
diff --git a/src/components/layout-header.jsx b/src/components/layout-header.jsx
index 73db28506..9db27dd87 100644
--- a/src/components/layout-header.jsx
+++ b/src/components/layout-header.jsx
@@ -1,17 +1,25 @@
/* global CONFIG */
import { IndexLink, Link, withRouter } from 'react-router';
-import { faBars, faSearch, faSignInAlt, faTimesCircle } from '@fortawesome/free-solid-svg-icons';
+import {
+ faBars,
+ faSearch,
+ faSignInAlt,
+ faSlidersH,
+ faTimesCircle,
+} from '@fortawesome/free-solid-svg-icons';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import cn from 'classnames';
import { KEY_ESCAPE } from 'keycode-js';
import { useDispatch, useSelector } from 'react-redux';
+import { useEvent } from 'react-use-event-hook';
import { openSidebar } from '../redux/action-creators';
import { Icon } from './fontawesome-icons';
import { useMediaQuery } from './hooks/media-query';
import styles from './layout-header.module.scss';
import { SignInLink } from './sign-in-link';
import { Autocomplete } from './autocomplete/autocomplete';
+import { ButtonLink } from './button-link';
const autocompleteAnchor = /(^|[^a-z\d])@|((from|to|author|by|in|commented-?by|liked-?by):)/gi;
@@ -57,6 +65,11 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
[router, query],
);
+ const showAdvancedSearch = useEvent(() => {
+ router.push(`/search?q=${encodeURIComponent(query.trim())}&advanced`);
+ input.current.blur();
+ });
+
const onKeyDown = useCallback((e) => e.keyCode === KEY_ESCAPE && input.current.blur(), []);
const clearSearchForm = useCallback(() => (setQuery(''), input.current.focus()), []);
@@ -91,6 +104,10 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
tabIndex={-1}
/>
+
+
+ Advanced search options
+
diff --git a/src/components/layout-header.module.scss b/src/components/layout-header.module.scss
index a0a102e04..489b24c06 100644
--- a/src/components/layout-header.module.scss
+++ b/src/components/layout-header.module.scss
@@ -108,6 +108,58 @@ $without-sidebar: '(max-width: 991px)';
width: 100%;
}
+.advancedSearch {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ border: 1px solid #999;
+ line-height: 2;
+ background: white;
+ box-shadow: 0 2px 4px rgb(0, 0, 0, 0.2);
+ display: flex;
+ gap: 0.5em;
+ padding: 0 0.5em;
+ align-items: center;
+ cursor: pointer;
+ opacity: 1;
+ translate: 0 0;
+ transition: all 0.1s;
+
+ &:last-child {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ &:hover {
+ background-color: #eee;
+ }
+
+ @starting-style {
+ opacity: 0;
+ translate: 0 -1em;
+ }
+
+ :global(.dark-theme) & {
+ background-color: $bg-color-lighter;
+ border-color: $separator-color;
+ }
+
+ :global(.dark-theme) &:hover {
+ background-color: $bg-color-lightest;
+ }
+}
+
+.advancedSearchIcon {
+ flex: none;
+ opacity: 0.5;
+}
+
+.searchInputContainer:not(:focus-within) .advancedSearch {
+ display: none;
+}
+
.searchInput {
width: 100%;
background-color: transparent;
diff --git a/src/components/search-feed.jsx b/src/components/search-feed.jsx
index 7aa4de5a5..6c9a7bd75 100644
--- a/src/components/search-feed.jsx
+++ b/src/components/search-feed.jsx
@@ -24,15 +24,21 @@ const AdvancedSearchForm = lazyComponent(
);
function FeedHandler(props) {
+ const urlQuery = useSelector((state) => state.routing.locationBeforeTransitions.query);
const queryString = useSearchQuery();
+ const preopenAdvancedForm = !queryString || 'advanced' in urlQuery;
+
const pageIsLoading = useSelector((state) => state.routeLoadingState);
- const [advFormVisible, setAdvFormVisible] = useBool(!queryString);
+ const [advFormVisible, setAdvFormVisible] = useBool(preopenAdvancedForm);
const authenticated = useSelector((state) => state.authenticated);
- useEffect(
- () => void (pageIsLoading && setAdvFormVisible(false)),
- [pageIsLoading, setAdvFormVisible],
- );
+ useEffect(() => {
+ if (pageIsLoading) {
+ setAdvFormVisible(false);
+ } else {
+ setAdvFormVisible(preopenAdvancedForm);
+ }
+ }, [pageIsLoading, preopenAdvancedForm, setAdvFormVisible]);
if (!authenticated) {
return (
From 47a02c5b35d1a8d47348decd5c79c4a1cea074ac Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Sat, 26 Oct 2024 14:51:33 +0300
Subject: [PATCH 08/25] Reword form text
---
.../advanced-search-form/advanced-search-form.jsx | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/components/advanced-search-form/advanced-search-form.jsx b/src/components/advanced-search-form/advanced-search-form.jsx
index 0f99c7fd7..d9205b53d 100644
--- a/src/components/advanced-search-form/advanced-search-form.jsx
+++ b/src/components/advanced-search-form/advanced-search-form.jsx
@@ -114,6 +114,7 @@ export function AdvancedSearchForm() {
+
-
{showFullForm ? (
<>
@@ -163,7 +163,11 @@ export function AdvancedSearchForm() {
placeholder="user1, user2"
filter="-from:"
/>
-
+
Date: Sat, 26 Oct 2024 14:53:09 +0300
Subject: [PATCH 09/25] Make 'select' paddings consistent with the other inputs
---
styles/common/common.scss | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/styles/common/common.scss b/styles/common/common.scss
index 2bd8faa8e..5696f85fa 100644
--- a/styles/common/common.scss
+++ b/styles/common/common.scss
@@ -17,3 +17,7 @@ pre,
samp {
font-family: Vazir, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
+
+select.form-control {
+ padding-inline: 0.6rem;
+}
From 3fcdc9438ce8345d1d984500ee65fa4f5fe8b2f1 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Sun, 27 Oct 2024 12:20:58 +0300
Subject: [PATCH 10/25] Add custom placeholder to the top search form
---
src/components/layout-header.jsx | 26 +++++++------
src/components/layout-header.module.scss | 47 ++++++++++++++++++++++--
2 files changed, 59 insertions(+), 14 deletions(-)
diff --git a/src/components/layout-header.jsx b/src/components/layout-header.jsx
index 9db27dd87..c72d2612a 100644
--- a/src/components/layout-header.jsx
+++ b/src/components/layout-header.jsx
@@ -95,7 +95,7 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
type="text"
name="q"
ref={input}
- placeholder="Search request"
+ placeholder=""
autoFocus={collapsibleSearchForm}
autoComplete="off"
value={query}
@@ -103,6 +103,20 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
onKeyDown={onKeyDown}
tabIndex={-1}
/>
+
+
+
+
+ Search request
+ /
+
@@ -112,16 +126,6 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
{compactSearchForm && }
-
-
-
{fullSearchForm && (
diff --git a/src/components/layout-header.module.scss b/src/components/layout-header.module.scss
index 489b24c06..a779b0107 100644
--- a/src/components/layout-header.module.scss
+++ b/src/components/layout-header.module.scss
@@ -261,14 +261,13 @@ $without-sidebar: '(max-width: 991px)';
height: 32px;
opacity: 0.4;
transition: opacity 0.3s;
- visibility: hidden;
&:hover {
opacity: 0.8;
}
- .searchInputContainer:focus-within & {
- visibility: visible;
+ .searchInput:placeholder-shown ~ & {
+ display: none;
}
}
@@ -283,3 +282,45 @@ $without-sidebar: '(max-width: 991px)';
align-items: center;
justify-content: center;
}
+
+.placeholderBox {
+ --placeholder-color: #999;
+
+ color: var(--placeholder-color);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ white-space: nowrap;
+ position: absolute;
+ inset: 0 0.5em;
+ pointer-events: none;
+
+ :global(.dark-theme) & {
+ --placeholder-color: #777;
+ }
+
+ @media (max-width: 699.9px) {
+ padding-left: 24px;
+ }
+
+ .searchInput:not(:placeholder-shown) ~ & {
+ display: none;
+ }
+
+ span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ kbd {
+ padding: 0 0.3em;
+ background-color: transparent;
+ color: inherit;
+ border: 1px solid;
+ box-shadow: none;
+
+ @media (max-width: 549px) {
+ display: none;
+ }
+ }
+}
From c6ed756748f186ef81987b4a0a366c9d832cf204 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Sun, 27 Oct 2024 12:22:54 +0300
Subject: [PATCH 11/25] Focus top search form on [/] or Ctrl+[K] keys
---
src/components/layout-header.jsx | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/src/components/layout-header.jsx b/src/components/layout-header.jsx
index c72d2612a..079d8ae15 100644
--- a/src/components/layout-header.jsx
+++ b/src/components/layout-header.jsx
@@ -86,6 +86,28 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
onBlur: closeSearchForm,
});
+ useEffect(() => {
+ const abortController = new AbortController();
+ document.addEventListener(
+ 'keydown',
+ (e) => {
+ if (document.activeElement !== document.body) {
+ return;
+ }
+ if (
+ // [/] or Ctrl+[K]
+ (e.code === 'Slash' && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) ||
+ (e.code === 'KeyK' && e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
+ ) {
+ input.current.focus();
+ e.preventDefault();
+ }
+ },
+ { signal: abortController.signal },
+ );
+ return () => abortController.abort();
+ }, []);
+
const searchForm = (
-
- Search request
- /
-
diff --git a/src/components/layout-header.module.scss b/src/components/layout-header.module.scss
index 42cd715b5..b0f16a7aa 100644
--- a/src/components/layout-header.module.scss
+++ b/src/components/layout-header.module.scss
@@ -282,45 +282,3 @@ $without-sidebar: '(max-width: 991px)';
align-items: center;
justify-content: center;
}
-
-.placeholderBox {
- --placeholder-color: #999;
-
- color: var(--placeholder-color);
- display: flex;
- align-items: center;
- justify-content: space-between;
- white-space: nowrap;
- position: absolute;
- inset: 0 0.5em;
- pointer-events: none;
-
- :global(.dark-theme) & {
- --placeholder-color: #777;
- }
-
- @media (max-width: 699.9px) {
- padding-left: 24px;
- }
-
- .searchInput:not(:placeholder-shown) ~ & {
- display: none;
- }
-
- span {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- kbd {
- padding: 0 0.3em;
- background-color: transparent;
- color: inherit;
- border: 1px solid;
- box-shadow: none;
-
- @media (max-width: 549px) {
- display: none;
- }
- }
-}
From cb7acfc64247019e9c0af50053a74f80f1554976 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Wed, 30 Oct 2024 20:12:43 +0300
Subject: [PATCH 15/25] Move top search form to the separate component
---
src/components/layout-header-search.jsx | 173 +++++++++++++++++++++++
src/components/layout-header.jsx | 179 +-----------------------
2 files changed, 180 insertions(+), 172 deletions(-)
create mode 100644 src/components/layout-header-search.jsx
diff --git a/src/components/layout-header-search.jsx b/src/components/layout-header-search.jsx
new file mode 100644
index 000000000..f1422b1e4
--- /dev/null
+++ b/src/components/layout-header-search.jsx
@@ -0,0 +1,173 @@
+import cn from 'classnames';
+import { faSearch, faSlidersH, faTimesCircle } from '@fortawesome/free-solid-svg-icons';
+import { useState, useEffect, useRef, useMemo } from 'react';
+import { useEvent } from 'react-use-event-hook';
+import { withRouter } from 'react-router';
+import { KEY_ESCAPE } from 'keycode-js';
+import styles from './layout-header.module.scss';
+import { Icon } from './fontawesome-icons';
+import { Autocomplete } from './autocomplete/autocomplete';
+import { ButtonLink } from './button-link';
+import { useMediaQuery } from './hooks/media-query';
+
+const autocompleteAnchor = /(^|[^a-z\d])@|((from|to|author|by|in|commented-?by|liked-?by):)/gi;
+
+export const HeaderSearchForm = withRouter(function HeaderSearchForm({ router, closeSearchForm }) {
+ const isWideScreen = useMediaQuery('(min-width: 700px)');
+ const isNarrowScreen = useMediaQuery('(max-width: 549px)');
+ const onSearchPage = router.routes[router.routes.length - 1].name === 'search';
+
+ const [searchExpanded, setSearchExpanded] = useState(false);
+ const [query, setQuery] = useState('');
+ const onQueryChange = useEvent(({ target }) => setQuery(target.value));
+
+ const fullSearchForm = isWideScreen;
+ const compactSearchForm = !fullSearchForm;
+ const collapsibleSearchForm = isNarrowScreen && (!onSearchPage || searchExpanded);
+
+ useEffect(() => {
+ if (!collapsibleSearchForm) {
+ setSearchExpanded(false);
+ }
+ }, [collapsibleSearchForm]);
+
+ const initialQuery = useInitialQuery(router);
+ const input = useRef(null);
+ useEffect(() => void setQuery(initialQuery), [initialQuery]);
+
+ const onSubmit = useEvent((e) => {
+ e.preventDefault();
+ const q = query.trim();
+ if (q !== '') {
+ router.push(`/search?q=${encodeURIComponent(q)}`);
+ input.current.blur();
+ }
+ });
+
+ const showAdvancedSearch = useEvent(() => {
+ router.push(`/search?q=${encodeURIComponent(query.trim())}&advanced`);
+ input.current.blur();
+ });
+
+ const onFocus = useEvent(() => isNarrowScreen && onSearchPage && setSearchExpanded(true));
+ const onKeyDown = useEvent((e) => e.keyCode === KEY_ESCAPE && input.current.blur());
+ const clearSearchForm = useEvent(() => (setQuery(''), input.current.focus()));
+
+ const focusHandlers = useDebouncedFocus({
+ onFocus,
+ onBlur: closeSearchForm,
+ });
+
+ useEffect(() => {
+ const abortController = new AbortController();
+ document.addEventListener(
+ 'keydown',
+ (e) => {
+ if (document.activeElement !== document.body) {
+ return;
+ }
+ if (
+ // [/] or Ctrl+[K]
+ (e.code === 'Slash' && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) ||
+ (e.code === 'KeyK' && e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
+ ) {
+ input.current.focus();
+ e.preventDefault();
+ }
+ },
+ { signal: abortController.signal },
+ );
+ return () => abortController.abort();
+ }, []);
+
+ return (
+
+ );
+});
+
+function useInitialQuery(router) {
+ return useMemo(() => {
+ const route = router.routes[router.routes.length - 1];
+ switch (route.name) {
+ case 'search':
+ return (router.location.query.q || router.location.query.qs || '').trim();
+ case 'saves':
+ return `in-my:saves `;
+ case 'discussions':
+ return `in-my:discussions `;
+ case 'direct':
+ return `in-my:directs `;
+ case 'userLikes':
+ return `liked-by:${router.params.userName} `;
+ case 'userComments':
+ return `commented-by:${router.params.userName} `;
+ case 'userFeed':
+ return `in:${router.params.userName} `;
+ default:
+ return '';
+ }
+ }, [router.routes, router.params, router.location]);
+}
+
+function useDebouncedFocus({ onFocus: onFocusOrig, onBlur: onBlurOrig }, interval = 100) {
+ const focusTimer = useRef(0);
+ const blurTimer = useRef(0);
+
+ const cleanup = useEvent(() => {
+ window.clearTimeout(blurTimer.current);
+ window.clearTimeout(focusTimer.current);
+ });
+ useEffect(() => () => cleanup(), [cleanup]);
+
+ const onFocus = useEvent(() => {
+ cleanup();
+ focusTimer.current = window.setTimeout(onFocusOrig, interval);
+ });
+ const onBlur = useEvent(() => {
+ cleanup();
+ blurTimer.current = window.setTimeout(onBlurOrig, interval);
+ });
+
+ return { onFocus, onBlur };
+}
diff --git a/src/components/layout-header.jsx b/src/components/layout-header.jsx
index e3e73d864..f56548bbe 100644
--- a/src/components/layout-header.jsx
+++ b/src/components/layout-header.jsx
@@ -1,27 +1,16 @@
/* global CONFIG */
import { IndexLink, Link, withRouter } from 'react-router';
-import {
- faBars,
- faSearch,
- faSignInAlt,
- faSlidersH,
- faTimesCircle,
-} from '@fortawesome/free-solid-svg-icons';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { faBars, faSearch, faSignInAlt } from '@fortawesome/free-solid-svg-icons';
+import { useCallback, useState } from 'react';
import cn from 'classnames';
-import { KEY_ESCAPE } from 'keycode-js';
import { useDispatch, useSelector } from 'react-redux';
-import { useEvent } from 'react-use-event-hook';
import { openSidebar } from '../redux/action-creators';
import { Icon } from './fontawesome-icons';
import { useMediaQuery } from './hooks/media-query';
import styles from './layout-header.module.scss';
import { SignInLink } from './sign-in-link';
-import { Autocomplete } from './autocomplete/autocomplete';
-import { ButtonLink } from './button-link';
-
-const autocompleteAnchor = /(^|[^a-z\d])@|((from|to|author|by|in|commented-?by|liked-?by):)/gi;
+import { HeaderSearchForm } from './layout-header-search';
export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
const dispatch = useDispatch();
@@ -33,126 +22,16 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
const authenticated = useSelector((state) => state.authenticated);
const [searchExpanded, setSearchExpanded] = useState(false);
- const [query, setQuery] = useState('');
- const onQueryChange = useCallback(({ target }) => setQuery(target.value), []);
const fullSearchForm = isWideScreen;
const compactSearchForm = !fullSearchForm;
const collapsibleSearchForm = isNarrowScreen && (!onSearchPage || searchExpanded);
- useEffect(() => {
- if (!collapsibleSearchForm) {
- setSearchExpanded(false);
- }
- }, [collapsibleSearchForm]);
-
const openSearchForm = useCallback(() => setSearchExpanded(true), []);
const closeSearchForm = useCallback(() => setSearchExpanded(false), []);
- const initialQuery = useInitialQuery(router);
- const input = useRef(null);
- useEffect(() => void setQuery(initialQuery), [initialQuery]);
-
- const onSubmit = useCallback(
- (e) => {
- e.preventDefault();
- const q = query.trim();
- if (q !== '') {
- router.push(`/search?q=${encodeURIComponent(q)}`);
- input.current.blur();
- }
- },
- [router, query],
- );
-
- const showAdvancedSearch = useEvent(() => {
- router.push(`/search?q=${encodeURIComponent(query.trim())}&advanced`);
- input.current.blur();
- });
-
- const onKeyDown = useCallback((e) => e.keyCode === KEY_ESCAPE && input.current.blur(), []);
-
- const clearSearchForm = useCallback(() => (setQuery(''), input.current.focus()), []);
-
- const onFocus = useCallback(
- () => isNarrowScreen && onSearchPage && setSearchExpanded(true),
- [isNarrowScreen, onSearchPage],
- );
-
const doOpenSidebar = useCallback(() => dispatch(openSidebar(true)), [dispatch]);
- const focusHandlers = useDebouncedFocus({
- onFocus,
- onBlur: closeSearchForm,
- });
-
- useEffect(() => {
- const abortController = new AbortController();
- document.addEventListener(
- 'keydown',
- (e) => {
- if (document.activeElement !== document.body) {
- return;
- }
- if (
- // [/] or Ctrl+[K]
- (e.code === 'Slash' && !e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) ||
- (e.code === 'KeyK' && e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
- ) {
- input.current.focus();
- e.preventDefault();
- }
- },
- { signal: abortController.signal },
- );
- return () => abortController.abort();
- }, []);
-
- const searchForm = (
-
- );
-
const sidebarButton =
!isLayoutWithSidebar &&
(authenticated ? (
@@ -186,7 +65,7 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
>
{searchExpanded ? (
- {authenticated && searchForm}
+ {authenticated ? : null}
{sidebarButton}
) : (
@@ -202,7 +81,9 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
)}
- {authenticated && !collapsibleSearchForm && searchForm}
+ {authenticated && !collapsibleSearchForm ? (
+
+ ) : null}
{authenticated && collapsibleSearchForm && (
);
});
-
-function useInitialQuery(router) {
- return useMemo(() => {
- const route = router.routes[router.routes.length - 1];
- switch (route.name) {
- case 'search':
- return (router.location.query.q || router.location.query.qs || '').trim();
- case 'saves':
- return `in-my:saves `;
- case 'discussions':
- return `in-my:discussions `;
- case 'direct':
- return `in-my:directs `;
- case 'userLikes':
- return `liked-by:${router.params.userName} `;
- case 'userComments':
- return `commented-by:${router.params.userName} `;
- case 'userFeed':
- return `in:${router.params.userName} `;
- default:
- return '';
- }
- }, [router.routes, router.params, router.location]);
-}
-
-function useDebouncedFocus({ onFocus: onFocusOrig, onBlur: onBlurOrig }, interval = 100) {
- const focusTimer = useRef(0);
- const blurTimer = useRef(0);
-
- const cleanup = useCallback(() => {
- window.clearTimeout(blurTimer.current);
- window.clearTimeout(focusTimer.current);
- }, []);
- useEffect(() => () => cleanup(), [cleanup]);
-
- const onFocus = useCallback(() => {
- cleanup();
- focusTimer.current = window.setTimeout(onFocusOrig, interval);
- }, [cleanup, onFocusOrig, interval]);
- const onBlur = useCallback(() => {
- cleanup();
- blurTimer.current = window.setTimeout(onBlurOrig, interval);
- }, [cleanup, onBlurOrig, interval]);
-
- return { onFocus, onBlur };
-}
From 35db1a33090b92d7b265b85c7b52e702f479bf8f Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Wed, 30 Oct 2024 20:27:55 +0300
Subject: [PATCH 16/25] Add a separate button to the "What to search" input
---
.../advanced-search-form.jsx | 27 +++++++++++++------
.../advanced-search-form.module.scss | 23 ++++++++++++++++
2 files changed, 42 insertions(+), 8 deletions(-)
diff --git a/src/components/advanced-search-form/advanced-search-form.jsx b/src/components/advanced-search-form/advanced-search-form.jsx
index d9205b53d..13676d0a7 100644
--- a/src/components/advanced-search-form/advanced-search-form.jsx
+++ b/src/components/advanced-search-form/advanced-search-form.jsx
@@ -1,3 +1,4 @@
+import cn from 'classnames';
import { browserHistory } from 'react-router';
import { useEffect, useMemo, useReducer, useState } from 'react';
import { useEvent } from 'react-use-event-hook';
@@ -81,13 +82,23 @@ export function AdvancedSearchForm() {
-
+
+
+
+ Search
+
+
Search for:
@@ -199,7 +210,7 @@ export function AdvancedSearchForm() {
Search
diff --git a/src/components/advanced-search-form/advanced-search-form.module.scss b/src/components/advanced-search-form/advanced-search-form.module.scss
index 196239a77..bb12638ea 100644
--- a/src/components/advanced-search-form/advanced-search-form.module.scss
+++ b/src/components/advanced-search-form/advanced-search-form.module.scss
@@ -46,6 +46,20 @@
margin-bottom: 0.5em;
}
+.searchInputBox {
+ display: flex;
+ gap: 0.3em;
+
+ button {
+ padding-inline: 2em;
+ display: none;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+ }
+}
+
.searchScopes {
margin-top: 0.5em;
display: flex;
@@ -148,3 +162,12 @@
.textInputChosen {
color: var(--text-color) !important;
}
+
+.bigSearchButton {
+ width: 100%;
+
+ @media (min-width: 768px) {
+ // Width of one column of the form
+ width: calc(50% - 1em);
+ }
+}
From 0e980bb11108a85714a23f66d69c005a6b09a2da Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 31 Oct 2024 10:47:29 +0300
Subject: [PATCH 17/25] Fix 'legend' color in dark mode
---
styles/helvetica/dark-theme.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/styles/helvetica/dark-theme.scss b/styles/helvetica/dark-theme.scss
index 10ee27a48..24a3f5d8f 100644
--- a/styles/helvetica/dark-theme.scss
+++ b/styles/helvetica/dark-theme.scss
@@ -57,6 +57,7 @@ body,
}
body,
+ legend,
.post .post-body .post-text,
.post .post-body,
.box-header,
From 4da16f3d4b01bdd1ed1f1c42283cb92ca81e70b4 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 31 Oct 2024 10:49:06 +0300
Subject: [PATCH 18/25] Add missed usernameFilters
---
src/components/advanced-search-form/helpers.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/components/advanced-search-form/helpers.js b/src/components/advanced-search-form/helpers.js
index f7ff8611c..96bdfa995 100644
--- a/src/components/advanced-search-form/helpers.js
+++ b/src/components/advanced-search-form/helpers.js
@@ -50,6 +50,8 @@ export const usernameFilters = new Set([
'by:',
'from:',
'commented-by:',
+ 'liked-by:',
+ 'cliked-by:',
'in:',
'-in:',
'-from:',
From e73f583722d571cced4c94b11232c3f40895d770 Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 31 Oct 2024 10:51:34 +0300
Subject: [PATCH 19/25] Allow to click on resulting search query
---
src/components/advanced-search-form/advanced-search-form.jsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/components/advanced-search-form/advanced-search-form.jsx b/src/components/advanced-search-form/advanced-search-form.jsx
index 13676d0a7..346a0dd63 100644
--- a/src/components/advanced-search-form/advanced-search-form.jsx
+++ b/src/components/advanced-search-form/advanced-search-form.jsx
@@ -218,7 +218,10 @@ export function AdvancedSearchForm() {
{resultingQuery ? (
- Search query: {resultingQuery}
+ Search query:{' '}
+
+ {resultingQuery}
+
) : null}
{showFullDocs ? (
From 5c207bc16c1ab2f34d67aff0e798023d0039df6a Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 31 Oct 2024 16:00:55 +0300
Subject: [PATCH 20/25] Make the behavior of the top search form the same on
all pages
---
src/components/layout-header-search.jsx | 12 ++----------
src/components/layout-header.jsx | 9 ++++-----
2 files changed, 6 insertions(+), 15 deletions(-)
diff --git a/src/components/layout-header-search.jsx b/src/components/layout-header-search.jsx
index f1422b1e4..8a5922573 100644
--- a/src/components/layout-header-search.jsx
+++ b/src/components/layout-header-search.jsx
@@ -15,21 +15,13 @@ const autocompleteAnchor = /(^|[^a-z\d])@|((from|to|author|by|in|commented-?by|l
export const HeaderSearchForm = withRouter(function HeaderSearchForm({ router, closeSearchForm }) {
const isWideScreen = useMediaQuery('(min-width: 700px)');
const isNarrowScreen = useMediaQuery('(max-width: 549px)');
- const onSearchPage = router.routes[router.routes.length - 1].name === 'search';
- const [searchExpanded, setSearchExpanded] = useState(false);
const [query, setQuery] = useState('');
const onQueryChange = useEvent(({ target }) => setQuery(target.value));
const fullSearchForm = isWideScreen;
const compactSearchForm = !fullSearchForm;
- const collapsibleSearchForm = isNarrowScreen && (!onSearchPage || searchExpanded);
-
- useEffect(() => {
- if (!collapsibleSearchForm) {
- setSearchExpanded(false);
- }
- }, [collapsibleSearchForm]);
+ const collapsibleSearchForm = isNarrowScreen;
const initialQuery = useInitialQuery(router);
const input = useRef(null);
@@ -49,7 +41,7 @@ export const HeaderSearchForm = withRouter(function HeaderSearchForm({ router, c
input.current.blur();
});
- const onFocus = useEvent(() => isNarrowScreen && onSearchPage && setSearchExpanded(true));
+ const onFocus = useEvent(() => {}); // TODO remove
const onKeyDown = useEvent((e) => e.keyCode === KEY_ESCAPE && input.current.blur());
const clearSearchForm = useEvent(() => (setQuery(''), input.current.focus()));
diff --git a/src/components/layout-header.jsx b/src/components/layout-header.jsx
index f56548bbe..e2cdb95f4 100644
--- a/src/components/layout-header.jsx
+++ b/src/components/layout-header.jsx
@@ -1,5 +1,5 @@
/* global CONFIG */
-import { IndexLink, Link, withRouter } from 'react-router';
+import { IndexLink, Link } from 'react-router';
import { faBars, faSearch, faSignInAlt } from '@fortawesome/free-solid-svg-icons';
import { useCallback, useState } from 'react';
import cn from 'classnames';
@@ -12,9 +12,8 @@ import styles from './layout-header.module.scss';
import { SignInLink } from './sign-in-link';
import { HeaderSearchForm } from './layout-header-search';
-export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
+export function LayoutHeader() {
const dispatch = useDispatch();
- const onSearchPage = router.routes[router.routes.length - 1].name === 'search';
const isLayoutWithSidebar = useMediaQuery('(min-width: 992px)');
const isWideScreen = useMediaQuery('(min-width: 700px)');
const isNarrowScreen = useMediaQuery('(max-width: 549px)');
@@ -25,7 +24,7 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
const fullSearchForm = isWideScreen;
const compactSearchForm = !fullSearchForm;
- const collapsibleSearchForm = isNarrowScreen && (!onSearchPage || searchExpanded);
+ const collapsibleSearchForm = isNarrowScreen;
const openSearchForm = useCallback(() => setSearchExpanded(true), []);
const closeSearchForm = useCallback(() => setSearchExpanded(false), []);
@@ -106,4 +105,4 @@ export const LayoutHeader = withRouter(function LayoutHeader({ router }) {
)}
);
-});
+}
From 829808797ecec42062342a66ccafb31bcd2617df Mon Sep 17 00:00:00 2001
From: David Mzareulyan
Date: Thu, 31 Oct 2024 16:30:51 +0300
Subject: [PATCH 21/25] Use proper onBlur (focusout) handler in top search form
---
src/components/layout-header-search.jsx | 33 +++++--------------------
1 file changed, 6 insertions(+), 27 deletions(-)
diff --git a/src/components/layout-header-search.jsx b/src/components/layout-header-search.jsx
index 8a5922573..d56f2e654 100644
--- a/src/components/layout-header-search.jsx
+++ b/src/components/layout-header-search.jsx
@@ -41,13 +41,14 @@ export const HeaderSearchForm = withRouter(function HeaderSearchForm({ router, c
input.current.blur();
});
- const onFocus = useEvent(() => {}); // TODO remove
const onKeyDown = useEvent((e) => e.keyCode === KEY_ESCAPE && input.current.blur());
const clearSearchForm = useEvent(() => (setQuery(''), input.current.focus()));
- const focusHandlers = useDebouncedFocus({
- onFocus,
- onBlur: closeSearchForm,
+ const onBlur = useEvent((e) => {
+ // When the new focus is outside the search form
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ closeSearchForm();
+ }
});
useEffect(() => {
@@ -74,7 +75,7 @@ export const HeaderSearchForm = withRouter(function HeaderSearchForm({ router, c
return (