added search

This commit is contained in:
Sergey Yarkov 2020-07-11 18:08:02 +03:00
parent a3fdcceb5c
commit 6d02cad50d
16 changed files with 179 additions and 25 deletions

View File

@ -130,6 +130,56 @@ module.exports = {
], ],
}, },
}, },
{
resolve: 'gatsby-plugin-local-search',
options: {
name: 'pages',
engine: 'flexsearch',
engineOptions: 'speed',
query: `
{
allMarkdownRemark(filter: { frontmatter: { templateKey: { eq: "article" } } }) {
nodes {
id
frontmatter {
slug
title
category
image {
childImageSharp {
fluid(maxWidth: 960) {
aspectRatio
base64
sizes
src
srcSet
}
}
}
tags
description
}
rawMarkdownBody
}
}
}
`,
ref: 'id',
index: ['title', 'category', 'body', 'tags', 'description'],
store: ['id', 'slug', 'title', 'category', 'tags', 'description', 'image'],
normalizer: ({ data }) =>
data.allMarkdownRemark.nodes.map(node => ({
id: node.id,
slug: node.frontmatter.slug,
title: node.frontmatter.title,
category: node.frontmatter.category,
description: node.frontmatter.description,
image: node.frontmatter.image,
tags: node.frontmatter.tags,
body: node.rawMarkdownBody,
})),
},
},
{ {
resolve: 'gatsby-plugin-react-svg', resolve: 'gatsby-plugin-react-svg',
options: { options: {

47
package-lock.json generated
View File

@ -8227,6 +8227,11 @@
"normalize.css": "^7.0.0" "normalize.css": "^7.0.0"
} }
}, },
"flexsearch": {
"version": "0.6.32",
"resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.6.32.tgz",
"integrity": "sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q=="
},
"flush-write-stream": { "flush-write-stream": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
@ -8717,6 +8722,25 @@
"micromatch": "^3.1.10" "micromatch": "^3.1.10"
} }
}, },
"gatsby-plugin-local-search": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/gatsby-plugin-local-search/-/gatsby-plugin-local-search-1.1.1.tgz",
"integrity": "sha512-OU0IR0Gi6ExZ3bzB0j6pR1rnnBPNGh3BEoRDAfvDeP9+U9rqzrPV053cr+Ueq0snEnlisPXu+vIv9Hd04esPnw==",
"requires": {
"flexsearch": "^0.6.32",
"lodash.lowerfirst": "^4.3.1",
"lodash.pick": "^4.4.0",
"lunr": "^2.3.8",
"pascalcase": "^1.0.0"
},
"dependencies": {
"pascalcase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-1.0.0.tgz",
"integrity": "sha512-BSExi0rRnCHReJys6NocaK+cfTXNinAegfWBvr0JD3hiaEG7Nuc7r0CIdOJunXrs8gU/sbHQ9dxVAtiVQisjmg=="
}
}
},
"gatsby-plugin-manifest": { "gatsby-plugin-manifest": {
"version": "2.4.17", "version": "2.4.17",
"resolved": "https://registry.npmjs.org/gatsby-plugin-manifest/-/gatsby-plugin-manifest-2.4.17.tgz", "resolved": "https://registry.npmjs.org/gatsby-plugin-manifest/-/gatsby-plugin-manifest-2.4.17.tgz",
@ -12703,6 +12727,11 @@
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
}, },
"lodash.lowerfirst": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/lodash.lowerfirst/-/lodash.lowerfirst-4.3.1.tgz",
"integrity": "sha1-3jx7EuAsZSSgBZwvbLfFxSZVoT0="
},
"lodash.map": { "lodash.map": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
@ -12718,6 +12747,11 @@
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
}, },
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
},
"lodash.throttle": { "lodash.throttle": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
@ -12908,6 +12942,11 @@
"yallist": "^2.0.0" "yallist": "^2.0.0"
} }
}, },
"lunr": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz",
"integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg=="
},
"magic-string": { "magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@ -17027,6 +17066,14 @@
} }
} }
}, },
"react-use-flexsearch": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/react-use-flexsearch/-/react-use-flexsearch-0.1.1.tgz",
"integrity": "sha512-UDRDB26HPcAo0gXNkUYYkcjoYCW4FSWr7Ich4adgVr7bqefJG7fnjlcqnwsKQkbZuteRLYzzox+1FKRTt3Z5vg==",
"requires": {
"flexsearch": "^0.6.22"
}
},
"react-virtualized-auto-sizer": { "react-virtualized-auto-sizer": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz",

View File

@ -19,6 +19,7 @@
"esm": "^3.2.25", "esm": "^3.2.25",
"gatsby": "^2.23.12", "gatsby": "^2.23.12",
"gatsby-image": "^2.4.12", "gatsby-image": "^2.4.12",
"gatsby-plugin-local-search": "^1.1.1",
"gatsby-plugin-manifest": "^2.4.17", "gatsby-plugin-manifest": "^2.4.17",
"gatsby-plugin-netlify-cms": "^4.3.9", "gatsby-plugin-netlify-cms": "^4.3.9",
"gatsby-plugin-react-helmet": "^3.3.8", "gatsby-plugin-react-helmet": "^3.3.8",
@ -36,11 +37,13 @@
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"prismjs": "^1.20.0", "prismjs": "^1.20.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^6.13.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-flexbox-grid": "^2.1.2", "react-flexbox-grid": "^2.1.2",
"react-github-btn": "^1.2.0", "react-github-btn": "^1.2.0",
"react-helmet": "^6.1.0" "react-helmet": "^6.1.0",
"react-use-flexsearch": "^0.1.1"
}, },
"devDependencies": { "devDependencies": {
"prettier": "2.0.5" "prettier": "2.0.5"

View File

@ -16,7 +16,7 @@ const Article = ({ data: { slug, keywords, title, image, description, category,
const { siteMetadata: { siteUrl } } = useSiteMetadataQuery() const { siteMetadata: { siteUrl } } = useSiteMetadataQuery()
return ( return (
<> <>
<SEO title={title} keywords={keywords.join(', ')} description={description} image={`${siteUrl}${image.childImageSharp.fluid.src}`} type='article' /> <SEO title={title} titleTemplate={false} keywords={keywords.join(', ')} description={description} image={`${siteUrl}${image.childImageSharp.fluid.src}`} type='article' />
<div className="article"> <div className="article">
<article> <article>
<div className="article-info"> <div className="article-info">
@ -31,7 +31,7 @@ const Article = ({ data: { slug, keywords, title, image, description, category,
</div> </div>
<div className="right-info"> <div className="right-info">
<div className="article-info__date"> <div className="article-info__date">
<span>{new Date(date).toLocaleDateString()}</span> <span>{date}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Link } from 'gatsby' import { Link } from 'gatsby'
const Categories = ({ children }) => { const Categories = ({ children, pathname }) => {
return ( return (
<div className="articles-categories"> <div className="articles-categories">
<div className="tag-item"> <div className={pathname === `/articles` ? 'tag-item active' : 'tag-item'}>
<Link to='/articles'>Все</Link> <Link to='/articles'>Все</Link>
</div> </div>
{children} {children}

View File

@ -1,13 +1,26 @@
import React from 'react' import React from 'react'
import { navigate } from 'gatsby'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
const Search = ({ articlesLength }) => { const Search = ({ articlesLength, resultsLength, query, setQuery }) => {
const handleInput = e => {
const query = e.target.value
if (query === '') {
navigate('/articles')
setQuery('')
return null
}
navigate(`/articles/?search=${query.trim()}`)
setQuery(query)
}
return ( return (
<div className="articles-search"> <div className="articles-search">
<input placeholder="Напишите сюда чтобы отфильтровать статьи" type="text" /> <input onChange={e => handleInput(e)} value={query} placeholder="Напишите сюда чтобы отфильтровать статьи" type="text" />
<span>{articlesLength}</span> <span>{query === '' ? articlesLength : resultsLength}</span>
</div> </div>
) )
} }
export default Search export default Search

View File

@ -30,7 +30,7 @@ const Header = () => {
const renderLinks = () => { const renderLinks = () => {
return ( return (
<ul> <ul>
{menuLinks.map((page, i) => <li key={i} className={page.link === pathname ? 'active' : null}><Link to={page.link}>{page.name}</Link></li>)} {menuLinks.map((page, i) => <li key={i} className={page.link === pathname || pathname === `${page.link}/` ? 'active' : null}><Link to={page.link}>{page.name}</Link></li>)}
</ul> </ul>
) )
} }

View File

@ -4,12 +4,13 @@ import PropTypes from 'prop-types'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useLocation } from '@reach/router' import { useLocation } from '@reach/router'
const SEO = ({ title, keywords, description, image, type, canonical }) => { const SEO = ({ title, titleTemplate, keywords, description, image, type, canonical }) => {
const { pathname } = useLocation() const { pathname } = useLocation()
const { siteMetadata } = useSiteMetadataQuery() const { siteMetadata } = useSiteMetadataQuery()
const seo = { const seo = {
title: title || siteMetadata.title, title: title || siteMetadata.title,
titleTemplate: true,
siteName: siteMetadata.siteName, siteName: siteMetadata.siteName,
type: type || 'website', type: type || 'website',
image: image || 'custom image url...', image: image || 'custom image url...',
@ -18,9 +19,10 @@ const SEO = ({ title, keywords, description, image, type, canonical }) => {
url: `${siteMetadata.siteUrl}${pathname}`, url: `${siteMetadata.siteUrl}${pathname}`,
canonical: canonical || `${siteMetadata.siteUrl}${pathname}` canonical: canonical || `${siteMetadata.siteUrl}${pathname}`
} }
return ( return (
<Helmet title={seo.title} titleTemplate={siteMetadata.titleTemplate}> <Helmet title={seo.title} titleTemplate={titleTemplate === false ? null : siteMetadata.titleTemplate}>
<html lang="ru" />
<meta name="description" content={seo.description} /> <meta name="description" content={seo.description} />
{seo.keywords && ( {seo.keywords && (
<meta name='keywords' content={seo.keywords} /> <meta name='keywords' content={seo.keywords} />

View File

@ -0,0 +1,13 @@
const toFrontmatter = array => {
return {
edges: array.map(el => {
return {
node: {
frontmatter: el
}
}
})
}
}
export default toFrontmatter

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" style="msTransform: rotate(360deg); WebkitTransform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path fill="#9AAAB4" d="M27.388 24.642L24.56 27.47l-4.95-4.95l2.828-2.828z"/><path fill="#66757F" d="M34.683 29.11l-5.879-5.879a2 2 0 0 0-2.828 0l-2.828 2.828a2 2 0 0 0 0 2.828l5.879 5.879a4 4 0 1 0 5.656-5.656z"/><circle fill="#8899A6" cx="13.586" cy="13.669" r="13.5"/><circle fill="#BBDDF5" cx="13.586" cy="13.669" r="9.5"/><rect x="0" y="0" width="36" height="36" fill="rgba(0, 0, 0, 0)" /></svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@ -2,11 +2,13 @@ import React from 'react';
import { Main } from '../ui/ui' import { Main } from '../ui/ui'
import { Grid } from 'react-flexbox-grid' import { Grid } from 'react-flexbox-grid'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import SEO from '../components/SEO/SEO'
import Layout from '../components/Layout/Layout' import Layout from '../components/Layout/Layout'
const NotFound = () => { const NotFound = () => {
return ( return (
<Layout> <Layout>
<SEO />
<Main> <Main>
<Grid fluid> <Grid fluid>
<div className="error"> <div className="error">

View File

@ -2,7 +2,12 @@ import React from "react"
import { Main } from '../ui/ui' import { Main } from '../ui/ui'
import { Grid, Row, Col } from 'react-flexbox-grid' import { Grid, Row, Col } from 'react-flexbox-grid'
import { graphql } from 'gatsby' import { graphql } from 'gatsby'
import { useFlexSearch } from 'react-use-flexsearch'
import { useLocation } from '@reach/router'
import * as queryString from 'query-string'
import toFrontmatter from '../helpers/toFrontmatter'
import useCategoriesQuery from '../hooks/useCategoriesQuery' import useCategoriesQuery from '../hooks/useCategoriesQuery'
import SEO from '../components/SEO/SEO' import SEO from '../components/SEO/SEO'
import Layout from '../components/Layout/Layout' import Layout from '../components/Layout/Layout'
import Articles from '../components/Articles/Articles' import Articles from '../components/Articles/Articles'
@ -12,8 +17,20 @@ import Search from '../components/Articles/Search/Search'
import Offers from '../components/Offers/Offers' import Offers from '../components/Offers/Offers'
import Newsletter from '../components/Newsletter/Newsletter' import Newsletter from '../components/Newsletter/Newsletter'
const ArticlesPage = ({ data: { allMarkdownRemark: edges } }) => { import SearchIcon from '../images/search-icon.svg'
const ArticlesPage = ({ data: { allMarkdownRemark: edges, localSearchPages }, }) => {
const { categories } = useCategoriesQuery() const { categories } = useCategoriesQuery()
const { search, pathname } = useLocation()
const [query, setQuery] = React.useState(queryString.parse(search).search || '')
const results = useFlexSearch(
query,
localSearchPages.index,
JSON.parse(localSearchPages.store)
)
return ( return (
<Layout> <Layout>
<SEO title='Статьи' /> <SEO title='Статьи' />
@ -25,16 +42,22 @@ const ArticlesPage = ({ data: { allMarkdownRemark: edges } }) => {
<div className="articles-heading"> <div className="articles-heading">
<h1>Статьи</h1> <h1>Статьи</h1>
</div> </div>
<Categories> <Categories pathname={pathname}>
{categories.map(({ node }, i) => <CategoryItem key={i} title={node.frontmatter.title} />)} {categories.map(({ node }, i) => <CategoryItem key={i} title={node.frontmatter.title} />)}
</Categories> </Categories>
<Search articlesLength={edges.edges.length} /> <Search articlesLength={edges.edges.length} resultsLength={results.length} query={query} setQuery={setQuery} />
<div className="articles-info">
{query ? results.length === 0 ? <p><SearchIcon />&nbsp;&nbsp;Ничего не найдено</p> : null : null}
</div>
</div> </div>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col lg={9} xs={12}> <Col lg={9} xs={12}>
<Articles data={edges} /> {query
? results.length > 0 ? <Articles data={toFrontmatter(results)} /> : null
: <Articles data={edges} />
}
<Newsletter /> <Newsletter />
</Col > </Col >
<Col lg={3} xs={12}> <Col lg={3} xs={12}>
@ -69,6 +92,10 @@ export const query = graphql`
} }
} }
} }
localSearchPages {
index
store
}
} }
` `

View File

@ -20,7 +20,7 @@ const IndexPage = ({ data: { allMarkdownRemark: edges } }) => {
<Col xs={12}> <Col xs={12}>
<div className="articles"> <div className="articles">
<div className="articles-heading"> <div className="articles-heading">
<h3>Последние статьи</h3> <h1>Последние статьи</h1>
<Link to='/articles'>смотреть все <ArrowNext /></Link> <Link to='/articles'>смотреть все <ArrowNext /></Link>
</div> </div>
</div> </div>

View File

@ -47,7 +47,7 @@ export const pageQuery = graphql`
time time
tags tags
description description
date date(formatString: "D MMMM YYYY", locale: "ru-RU")
category category
} }
} }

View File

@ -7,7 +7,6 @@ import useCategoriesQuery from '../hooks/useCategoriesQuery'
import SEO from '../components/SEO/SEO' import SEO from '../components/SEO/SEO'
import Layout from '../components/Layout/Layout' import Layout from '../components/Layout/Layout'
import Articles from '../components/Articles/Articles' import Articles from '../components/Articles/Articles'
import Search from '../components/Articles/Search/Search'
import Categories from '../components/Articles/Categories/Categories' import Categories from '../components/Articles/Categories/Categories'
import CategoryItem from '../components/Articles/Categories/CategoryItem' import CategoryItem from '../components/Articles/Categories/CategoryItem'
import Offers from '../components/Offers/Offers' import Offers from '../components/Offers/Offers'
@ -26,12 +25,11 @@ const CategoryPage = ({ data: { allMarkdownRemark: edges }, pageContext: { title
<Col lg={9} xs={12}> <Col lg={9} xs={12}>
<div className="articles"> <div className="articles">
<div className="articles-heading"> <div className="articles-heading">
<h1>Статьи</h1> <h1>Категория: <u>{title}</u></h1>
</div> </div>
<Categories> <Categories>
{categories.map(({ node }, i) => <CategoryItem key={i} title={node.frontmatter.title} />)} {categories.map(({ node }, i) => <CategoryItem key={i} title={node.frontmatter.title} />)}
</Categories> </Categories>
<Search articlesLength={edges.edges.length} />
<div className="articles-info"> <div className="articles-info">
<p>Показано {edges.edges.length} из категории "{title}"</p> <p>Показано {edges.edges.length} из категории "{title}"</p>
</div> </div>

View File

@ -7,7 +7,6 @@ import useCategoriesQuery from '../hooks/useCategoriesQuery'
import SEO from '../components/SEO/SEO' import SEO from '../components/SEO/SEO'
import Layout from '../components/Layout/Layout' import Layout from '../components/Layout/Layout'
import Articles from '../components/Articles/Articles' import Articles from '../components/Articles/Articles'
import Search from '../components/Articles/Search/Search'
import Categories from '../components/Articles/Categories/Categories' import Categories from '../components/Articles/Categories/Categories'
import CategoryItem from '../components/Articles/Categories/CategoryItem' import CategoryItem from '../components/Articles/Categories/CategoryItem'
import Offers from '../components/Offers/Offers' import Offers from '../components/Offers/Offers'
@ -26,12 +25,11 @@ const TagPage = ({ data: { allMarkdownRemark: edges }, pageContext: { title } })
<Col lg={9} xs={12}> <Col lg={9} xs={12}>
<div className="articles"> <div className="articles">
<div className="articles-heading"> <div className="articles-heading">
<h1>Статьи</h1> <h1>Тэг: <u>{title}</u></h1>
</div> </div>
<Categories> <Categories>
{categories.map(({ node }, i) => <CategoryItem key={i} title={node.frontmatter.title} />)} {categories.map(({ node }, i) => <CategoryItem key={i} title={node.frontmatter.title} />)}
</Categories> </Categories>
<Search articlesLength={edges.edges.length} />
<div className="articles-info"> <div className="articles-info">
<p>Показано {edges.edges.length} по тегу "{title}"</p> <p>Показано {edges.edges.length} по тегу "{title}"</p>
</div> </div>