diff --git a/.circleci/config.yml b/.circleci/config.yml index ee3cb562..3d30e549 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ aliases: defaults: &defaults working_directory: ~/repo docker: - - image: circleci/node:12.14.1 + - image: cimg/node:14.18.2 version: 2 jobs: diff --git a/.eslintrc.js b/.eslintrc.js index bcfa57ae..a152caf0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,11 +1,22 @@ module.exports = { extends: ['algolia', 'algolia/react'], rules: { - 'react/prop-types': 0, - 'no-param-reassign': 0, 'eslint-comments/disable-enable-pair': 0, + // Do not enforce a specific import order + 'import/order': 0, + // Allow function components + 'react/function-component-definition': 0, + // Allow boolean props without explicit values + 'react/jsx-boolean-value': 0, + // Allow JSX content in .js files + 'react/jsx-filename-extension': 0, + // Do not enforce event listeners naming conventions + 'react/jsx-handler-names': 0, + // Allow passing function references to event listeners + 'react/jsx-no-bind': 0, // Avoid errors about `UNSAFE` lifecycles (e.g. `UNSAFE_componentWillMount`) 'react/no-deprecated': 0, + 'react/prop-types': 0, 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', }, @@ -14,4 +25,10 @@ module.exports = { version: 'detect', }, }, + parserOptions: { + requireConfigFile: false, + babelOptions: { + presets: ['@babel/preset-react'], + }, + }, }; diff --git a/.github/unified-instantsearch-ecommerce.png b/.github/unified-instantsearch-ecommerce.png new file mode 100644 index 00000000..90e3aee1 Binary files /dev/null and b/.github/unified-instantsearch-ecommerce.png differ diff --git a/.nvmrc b/.nvmrc index d4c43fa7..d630e949 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.14.1 \ No newline at end of file +14.18.2 \ No newline at end of file diff --git a/.stylelintrc.json b/.stylelintrc.json index 25b3e5b2..613ea19e 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -11,9 +11,9 @@ "stylelint-prettier/recommended" ], "rules": { - "selector-class-pattern": [ - "^(?:ais-|uni-)[A-Z][a-zA-Z0-9]+[-a-zA-Z0-9]+$|^(?!ais-|uni-)(?:[a-z][a-z0-9]+)(?:-[a-z0-9]+)+$" - ], + "alpha-value-notation": "number", + "color-function-notation": "legacy", + "selector-class-pattern": "^(?:ais-|uni-)[A-Z][a-zA-Z0-9]+[-a-zA-Z0-9]+$|^(?!ais-|uni-)(?:[a-z][a-z0-9]+)(?:-[a-z0-9]+)+$", "prettier/prettier": true, "max-nesting-depth": [ 2, @@ -31,6 +31,12 @@ { "severity": "warning" } + ], + "value-keyword-case": [ + "lower", + { + "ignoreKeywords": ["currentColor", "optimizeSpeed"] + } ] } } diff --git a/README.md b/README.md index 6c43afc9..3bfc4f27 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Unified InstantSearch E-Commerce

- +Unified InstantSearch E-Commerce

**This project aims to be used by any e-commerce website to bootstrap a search experience powered by Algolia the fastest way possible.** diff --git a/package.json b/package.json index ae81af8c..093d92b0 100644 --- a/package.json +++ b/package.json @@ -8,68 +8,69 @@ "export": "NODE_ENV=production webpack --config webpack/build.babel.js", "preview": "NODE_ENV=production webpack --config webpack/preview.babel.js", "lint": "run-p -c lint:*", - "lint:js": "eslint --ext .js .", + "lint:js": "eslint --ext .js src", "lint:css": "stylelint '**/*.(s)css'", "release": "shipjs prepare" }, "dependencies": { - "algoliasearch": "4.1.0", + "algoliasearch": "4.12.2", "invariant": "2.2.4", - "preact": "10.4.0", - "qs": "6.9.3", - "react-instantsearch-dom": "6.4.0", - "react-router-dom": "5.1.2", + "preact": "10.6.6", + "qs": "6.10.3", + "react-instantsearch-dom": "6.22.0", + "react-router-dom": "6.2.2", "rheostat": "2.2.0", "search-insights": "1.4.0" }, "devDependencies": { - "@babel/core": "7.9.0", - "@babel/preset-env": "7.9.5", - "@babel/preset-react": "7.9.4", - "@babel/register": "7.9.0", - "babel-eslint": "10.1.0", - "babel-loader": "8.1.0", + "@babel/core": "7.17.5", + "@babel/eslint-parser": "7.17.0", + "@babel/preset-env": "7.16.11", + "@babel/preset-react": "7.16.7", + "@babel/register": "7.17.0", + "babel-loader": "8.2.3", "babel-plugin-strip-invariant": "1.0.0", - "clean-webpack-plugin": "3.0.0", - "copy-webpack-plugin": "5.1.1", - "css-loader": "3.5.2", - "eslint": "6.8.0", - "eslint-config-algolia": "15.0.0", - "eslint-config-prettier": "6.10.1", - "eslint-plugin-eslint-comments": "3.1.2", - "eslint-plugin-import": "2.20.2", - "eslint-plugin-jest": "23.8.2", - "eslint-plugin-prettier": "3.1.3", - "eslint-plugin-react": "7.19.0", - "eslint-plugin-react-hooks": "3.0.0", - "html-webpack-plugin": "4.3.0", + "copy-webpack-plugin": "10.2.4", + "css-loader": "6.6.0", + "css-minimizer-webpack-plugin": "3.4.1", + "eslint": "8.10.0", + "eslint-config-algolia": "20.0.0", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-eslint-comments": "3.2.0", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-jest": "26.1.1", + "eslint-plugin-jsdoc": "37.9.6", + "eslint-plugin-jsx-a11y": "6.5.1", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-react": "7.29.2", + "eslint-plugin-react-hooks": "4.3.0", + "html-webpack-plugin": "5.5.0", "ignore-styles": "5.0.1", - "mini-css-extract-plugin": "0.9.0", + "mini-css-extract-plugin": "2.5.3", "module-alias": "2.2.2", - "node-sass": "4.13.1", "npm-run-all": "4.1.5", - "optimize-css-assets-webpack-plugin": "5.0.3", - "postcss": "7.0.27", + "postcss": "8.4.7", "postcss-inject-css-variables": "0.0.2", - "postcss-loader": "3.0.0", - "postcss-preset-env": "6.7.0", - "prettier": "2.0.4", - "sass-loader": "8.0.2", - "shipjs": "0.21.0", - "style-loader": "1.2.1", - "stylelint": "13.3.2", + "postcss-loader": "6.2.1", + "postcss-preset-env": "7.4.2", + "prettier": "2.5.1", + "sass": "1.49.9", + "sass-loader": "12.6.0", + "shipjs": "0.24.3", + "style-loader": "3.3.1", + "stylelint": "14.5.3", "stylelint-a11y": "1.2.3", - "stylelint-config-prettier": "8.0.1", - "stylelint-config-sass-guidelines": "7.0.0", - "stylelint-config-standard": "20.0.0", - "stylelint-no-unsupported-browser-features": "4.0.0", - "stylelint-order": "4.0.0", - "stylelint-prettier": "1.1.2", - "terser-webpack-plugin": "2.3.6", - "webpack": "4.43.0", - "webpack-cli": "3.3.11", - "webpack-dev-server": "3.10.3", - "webpack-merge": "4.2.2" + "stylelint-config-prettier": "9.0.3", + "stylelint-config-sass-guidelines": "9.0.1", + "stylelint-config-standard": "25.0.0", + "stylelint-no-unsupported-browser-features": "5.0.3", + "stylelint-order": "5.0.0", + "stylelint-prettier": "2.0.0", + "terser-webpack-plugin": "5.3.1", + "webpack": "5.69.1", + "webpack-cli": "4.9.2", + "webpack-dev-server": "4.7.4", + "webpack-merge": "5.8.0" }, "_moduleAliases": { "react": "node_modules/preact/compat" diff --git a/postcss.config.js b/postcss.config.js index 391aeeb3..21e852eb 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,9 +1,5 @@ /* eslint-disable import/no-commonjs, no-global-assign */ -// When the configuration file refers to `window`, we need to shim it so it -// doesn't break when processed with Node during the build step. -window = global; - require('module-alias/register'); require('ignore-styles'); diff --git a/src/App.js b/src/App.js index 8bf40925..18d3be29 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,6 @@ import React, { createPortal } from 'preact/compat'; import { connectHitInsights } from 'react-instantsearch-dom'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { getUrlFromState, getStateFromUrl, createURL } from './router'; import { useSearchClient, useInsights, useMatchMedia } from './hooks'; @@ -10,7 +10,7 @@ export const AppContext = React.createContext(null); export const SearchContext = React.createContext(null); export function App({ config }) { - const history = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const searchClient = useSearchClient(config); const { aa, userToken } = useInsights( @@ -60,10 +60,9 @@ export function App({ config }) { clearTimeout(lastSetStateId.current); lastSetStateId.current = setTimeout(() => { - history.push( - getUrlFromState({ location }, nextSearchState), - nextSearchState - ); + navigate(getUrlFromState({ location }, nextSearchState), { + state: nextSearchState, + }); if (config.googleAnalytics) { window.ga('send', 'pageView', `?query=${nextSearchState.query}`); @@ -114,11 +113,11 @@ export function App({ config }) { setSearchState(nextSearchState); if (JSON.stringify(searchState) !== JSON.stringify(nextSearchState)) { - history.push('', nextSearchState); + navigate('', { state: nextSearchState }); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOverlayShowing, setSearchState, history]); + }, [isOverlayShowing, setSearchState, navigate]); React.useEffect(() => { if (topAnchor.current) { @@ -196,6 +195,7 @@ export function App({ config }) { diff --git a/src/App.scss b/src/App.scss index 287591e8..f9b2771a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -15,8 +15,9 @@ @media screen and (prefers-reduced-motion: reduce) { display: none; } + &::after { - animation: loadingProgress 6s cubic-bezier(0.2, 0.7, 0.4, 1) forwards; + animation: loading-progress 6s cubic-bezier(0.2, 0.7, 0.4, 1) forwards; background: var(--algolia-theme-color-primary); box-shadow: inset 0 2px 2px rgba(255, 255, 255, 0.3); content: ''; @@ -31,10 +32,11 @@ } } -@keyframes loadingProgress { +@keyframes loading-progress { 0% { transform: scaleX(0); } + 100% { transform: scaleX(95%); } @@ -79,6 +81,7 @@ position: sticky; top: 0; z-index: 2; + .ais-SearchBox-completion { align-items: center; color: #777; @@ -86,13 +89,16 @@ top: 50%; transform: translateY(-50%); } + .ais-SearchBox-submit { padding: 4px 1rem 0 1.5rem; width: 48px; } + .ais-SearchBox { width: 100%; } + .ais-SearchBox-inputContainer { background: #ebecf3; background-image: linear-gradient( @@ -106,6 +112,7 @@ height: 100%; height: 64px; } + .ais-SearchBox-input, .ais-SearchBox-completion { background: none; @@ -115,8 +122,10 @@ position: absolute; width: 100%; } + .ais-SearchBox-input { color: inherit; + &::placeholder { color: #21243d; @@ -149,6 +158,7 @@ right: 0; top: 0; width: 65px; + &:hover, &:focus { color: #fff; @@ -167,9 +177,11 @@ @media (--algolia-theme-breakpoint-sm-max) { padding-top: 1rem; } + * { outline-color: #fff; } + em, mark { background: none; @@ -204,6 +216,7 @@ .uni-QuerySuggestions-item { line-height: 1; margin: 0.25rem 0; + &:not(:last-of-type) { margin-right: 0.5rem; } @@ -263,6 +276,7 @@ cursor: pointer; margin-right: -0.5rem; padding: 0.5rem; + svg { height: auto; width: 20px; @@ -399,9 +413,11 @@ @media (--algolia-theme-breakpoint-md-max) { margin-left: 0; } + .uni-Label { white-space: nowrap; } + .ais-SortBy { display: flex; flex-wrap: nowrap; @@ -415,6 +431,7 @@ padding-top: 0.2rem; width: 100%; } + > * + * { margin-left: 1rem; } @@ -434,9 +451,11 @@ cursor: pointer; padding: 0; padding: 0.5rem; + &[disabled] { cursor: not-allowed; } + &:not([disabled]):hover, &:not([disabled]):focus { border-color: #ddd; @@ -496,6 +515,7 @@ margin-left: 1rem; margin-right: 0; position: relative; + &::before { align-items: center; color: rgba(33, 36, 61, 0.32); @@ -506,6 +526,7 @@ position: absolute; right: 38px; } + &:checked { &::before { color: var(--algolia-theme-color-primary); @@ -597,6 +618,7 @@ bottom: 0; box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.62), inset 0 0 1px rgba(255, 255, 255, 0.9); + /* stylelint-disable-next-line custom-property-pattern */ font-family: var(--algolia-theme-text-fontFamily); -webkit-font-smoothing: antialiased; left: 0; @@ -606,27 +628,34 @@ position: fixed; right: 0; top: 0; + * { outline-color: var(--algolia-theme-color-primary); } @media (--algolia-theme-breakpoint-md-max) { margin: 0; + &.uni-Container--filtering { overflow: hidden; + .uni-LeftPanel { pointer-events: auto; } + .uni-LeftPanel-Overlay { opacity: 1; pointer-events: auto; } + .uni-Refinements { overflow-y: hidden; transform: translateX(0); } + .uni-Refinements-scrollable { overflow-y: scroll; } + .uni-FiltersButton { z-index: auto; } @@ -649,6 +678,7 @@ font-size: 1.2rem; font-weight: normal; margin-bottom: 1rem; + em { font-style: normal; font-weight: bold; @@ -657,6 +687,7 @@ .uni-NoResults-ResultSuggestionTitle { padding: 1rem 0; + em { font-style: normal; font-weight: bold; diff --git a/src/components/ColorList.js b/src/components/ColorList.js index b990d9b9..d57a532b 100644 --- a/src/components/ColorList.js +++ b/src/components/ColorList.js @@ -12,9 +12,9 @@ export const ColorList = connectRefinementList(function ColorList(props) { props.items.sort((a, b) => { if (a.label < b.label) { return -1; - } else { - return 1; } + + return 1; }); return ( diff --git a/src/components/ColorList.scss b/src/components/ColorList.scss index d8810d75..def8c7e9 100644 --- a/src/components/ColorList.scss +++ b/src/components/ColorList.scss @@ -3,15 +3,18 @@ border-radius: 50%; height: 24px; width: 24px; + &:hover, &:focus { border: 1px solid currentColor; box-shadow: inset 0 0 0 1px #fff; } } + .ais-RefinementList-labelText { margin-top: 3px; } + .ais-RefinementList-count { margin-top: 6px; } diff --git a/src/components/CurrentRefinements.js b/src/components/CurrentRefinements.js index 9b8aa92d..8cc90d3e 100644 --- a/src/components/CurrentRefinements.js +++ b/src/components/CurrentRefinements.js @@ -114,6 +114,7 @@ export const CurrentRefinements = connectCurrentRefinements( )} diff --git a/src/components/NoResultsHandler.js b/src/components/NoResultsHandler.js index 3d285838..f9802394 100644 --- a/src/components/NoResultsHandler.js +++ b/src/components/NoResultsHandler.js @@ -1,4 +1,4 @@ -import React from 'preact/compat'; +import React, { memo } from 'preact/compat'; import { Index, Configure, @@ -54,7 +54,7 @@ export const NoResultsHandler = connectStateResults(function ResultsWrapper( return props.children; }); -const NoResults = React.memo( +const NoResults = memo( function NoResults(props) { return (
@@ -91,6 +91,7 @@ const ResultsInAllCategories = connectCurrentRefinements(function ClearFilters(

Check the spelling, try a more general term, or{' '} diff --git a/src/components/ReverseHighlight.js b/src/components/ReverseHighlight.js index cfe926d8..5ad3f685 100644 --- a/src/components/ReverseHighlight.js +++ b/src/components/ReverseHighlight.js @@ -28,6 +28,7 @@ export function ReverseHighlight({ if (part.isHighlighted) { return React.createElement( tagName, + // eslint-disable-next-line react/no-array-index-key { key: index, className: 'ais-Highlight-highlighted' }, part.value ); @@ -35,6 +36,7 @@ export function ReverseHighlight({ return React.createElement( 'span', + // eslint-disable-next-line react/no-array-index-key { key: index, className: 'ais-Highlight-nonHighlighted' }, part.value ); diff --git a/src/components/Search.js b/src/components/Search.js index 5a57da23..323dfa4f 100644 --- a/src/components/Search.js +++ b/src/components/Search.js @@ -43,8 +43,8 @@ export function Search(props) { searchClient={props.searchClient} indexName={props.indexName} searchState={props.searchState} - onSearchStateChange={props.onSearchStateChange} createURL={props.createURL} + onSearchStateChange={props.onSearchStateChange} > @@ -55,6 +55,7 @@ export function Search(props) { diff --git a/src/components/SearchBox/FacetSearchBox.js b/src/components/SearchBox/FacetSearchBox.js index 5a4d39dc..cbcba972 100644 --- a/src/components/SearchBox/FacetSearchBox.js +++ b/src/components/SearchBox/FacetSearchBox.js @@ -4,10 +4,10 @@ export const FacetSearchBox = (props) => { return (

diff --git a/src/components/SearchBox/PredictiveSearchBox.js b/src/components/SearchBox/PredictiveSearchBox.js index 829e184b..1783d84d 100644 --- a/src/components/SearchBox/PredictiveSearchBox.js +++ b/src/components/SearchBox/PredictiveSearchBox.js @@ -104,6 +104,7 @@ const Suggestions = connectHits(function Suggestions({ return (