diff --git a/gatsby-config.js b/gatsby-config.js index 498c8ff4..999b86e0 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -99,10 +99,10 @@ module.exports = { // Fields to index. If store === true value will be stored in index file. // Attributes for custom indexing logic. See https://lunrjs.com/docs/lunr.Builder.html for details fields: [ - { name: 'title', store: true, attributes: { boost: 20 } }, + { name: 'title', attributes: { boost: 20 } }, { name: 'tags', attributes: { boost: 15 } }, - { name: 'slug', store: true }, { name: 'excerpt', attributes: { boost: 10 } }, + { name: 'slug', store: true }, { name: 'content' } ], // How to resolve each field's value for a supported node type diff --git a/src/components/Post/PostTeaser.jsx b/src/components/Post/PostTeaser.jsx new file mode 100644 index 00000000..c66deb1a --- /dev/null +++ b/src/components/Post/PostTeaser.jsx @@ -0,0 +1,36 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { Link } from 'gatsby' +import Image from '../atoms/Image' +import styles from './PostTeaser.module.scss' + +export default class PostTeaser extends PureComponent { + static propTypes = { + post: PropTypes.object.isRequired, + toggleSearch: PropTypes.func + } + + render() { + const { post, toggleSearch } = this.props + + return ( +
  • + + {post.frontmatter.image ? ( + <> + {post.frontmatter.title} +

    {post.frontmatter.title}

    + + ) : ( +
    +

    {post.frontmatter.title}

    +
    + )} + +
  • + ) + } +} diff --git a/src/components/Post/PostTeaser.module.scss b/src/components/Post/PostTeaser.module.scss new file mode 100644 index 00000000..749f70cf --- /dev/null +++ b/src/components/Post/PostTeaser.module.scss @@ -0,0 +1,29 @@ +@import 'variables'; + +.postTitle { + display: inline-block; + margin-top: $spacer / 4; + margin-bottom: 0; + font-size: $font-size-small; + line-height: $line-height-small; + color: $brand-grey-light; + padding-left: .2rem; + padding-right: .2rem; + transition: color .2s ease-out; + + @media (min-width: $screen-md) { + font-size: $font-size-base; + } +} + +.empty { + height: 100%; + min-height: 80px; + display: flex; + align-items: center; + padding: $spacer / 4; + + .postTitle { + margin-top: 0; + } +} diff --git a/src/components/Search/Search.jsx b/src/components/Search/Search.jsx index 55601c16..2a79a304 100644 --- a/src/components/Search/Search.jsx +++ b/src/components/Search/Search.jsx @@ -34,7 +34,9 @@ export default class Search extends PureComponent { search = event => { const query = event.target.value - const results = this.getSearchResults(query) + // wildcard search https://lunrjs.com/guides/searching.html#wildcards + const results = query.length > 1 ? this.getSearchResults(`${query}*`) : [] + this.setState({ results, query @@ -53,6 +55,7 @@ export default class Search extends PureComponent { + - + + )} diff --git a/src/components/Search/Search.module.scss b/src/components/Search/Search.module.scss index a21e1ee8..ae573f17 100644 --- a/src/components/Search/Search.module.scss +++ b/src/components/Search/Search.module.scss @@ -43,10 +43,4 @@ :global(.hasSearchOpen) { overflow: hidden; - - // more cross-browser backdrop-filter - main > div:first-child { - transition: filter .85s ease-out; - filter: blur(5px); - } } diff --git a/src/components/Search/SearchInput.jsx b/src/components/Search/SearchInput.jsx index 6148a478..6651e9ce 100644 --- a/src/components/Search/SearchInput.jsx +++ b/src/components/Search/SearchInput.jsx @@ -1,24 +1,26 @@ -import React from 'react' +import React, { PureComponent } from 'react' import Input from '../atoms/Input' import styles from './SearchInput.module.scss' -const SearchInput = ({ onToggle, ...props }) => ( - <> - - - -) - -export default SearchInput +export default class SearchInput extends PureComponent { + render() { + return ( + <> + + + + ) + } +} diff --git a/src/components/Search/SearchInput.module.scss b/src/components/Search/SearchInput.module.scss index 68376949..9e252de6 100644 --- a/src/components/Search/SearchInput.module.scss +++ b/src/components/Search/SearchInput.module.scss @@ -16,7 +16,7 @@ .searchInputClose { position: absolute; right: $spacer / 2; - top: $spacer / 4; + top: $spacer / 5; font-size: $font-size-h3; color: $brand-grey-light; diff --git a/src/components/Search/SearchResults.jsx b/src/components/Search/SearchResults.jsx index d4b9c788..29b135e0 100644 --- a/src/components/Search/SearchResults.jsx +++ b/src/components/Search/SearchResults.jsx @@ -1,31 +1,97 @@ -import React from 'react' +import React, { PureComponent } from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' -import { Link } from 'gatsby' +import { graphql, StaticQuery } from 'gatsby' import Container from '../atoms/Container' +import PostTeaser from '../Post/PostTeaser' import styles from './SearchResults.module.scss' -const SearchResults = ({ results, onClose }) => - ReactDOM.createPortal( -
    - -
      - {results.length > 0 && - results.map(page => ( -
    • - - {page.title} - -
    • - ))} -
    -
    -
    , - document.getElementById('document') - ) +const SearchEmpty = ({ searchQuery, results }) => ( +
    +
    +

    + {searchQuery.length > 1 && results.length === 0 + ? 'No results found' + : searchQuery.length === 1 + ? 'Just one more character' + : 'Awaiting your input'} +

    +
    +
    +) -SearchResults.propTypes = { - results: PropTypes.array.isRequired +SearchEmpty.propTypes = { + results: PropTypes.array.isRequired, + searchQuery: PropTypes.string.isRequired } -export default SearchResults +const query = graphql` + query { + allMarkdownRemark { + edges { + node { + id + frontmatter { + title + image { + childImageSharp { + ...ImageFluidThumb + } + } + } + fields { + slug + } + } + } + } + } +` + +export default class SearchResults extends PureComponent { + static propTypes = { + results: PropTypes.array.isRequired, + searchQuery: PropTypes.string.isRequired, + toggleSearch: PropTypes.func.isRequired + } + + render() { + const { searchQuery, results, toggleSearch } = this.props + + return ( + { + const posts = data.allMarkdownRemark.edges + + // creating portal to break out of DOM node we're in + // and render the results in content container + return ReactDOM.createPortal( +
    + + {results.length > 0 ? ( +
      + {results.map(page => + posts + .filter(post => post.node.fields.slug === page.slug) + .map(({ node }) => ( + + )) + )} +
    + ) : ( + + )} +
    +
    , + document.getElementById('document') + ) + }} + /> + ) + } +} diff --git a/src/components/Search/SearchResults.module.scss b/src/components/Search/SearchResults.module.scss index 86beabe8..208a7488 100644 --- a/src/components/Search/SearchResults.module.scss +++ b/src/components/Search/SearchResults.module.scss @@ -9,33 +9,80 @@ top: 0; bottom: 0; background: rgba($body-background-color, .95); - // backdrop-filter: blur(5px); + backdrop-filter: blur(5px); animation: fadein .3s; + overflow: scroll; + -webkit-overflow-scrolling: touch; + height: 91vh; ul { @include breakoutviewport; - padding-top: $spacer; - padding-bottom: $spacer; + padding: $spacer $spacer / 2; margin-bottom: 0; - overflow: scroll; - -webkit-overflow-scrolling: touch; - max-height: 81vh; + display: flex; + flex-wrap: wrap; + justify-content: space-between; li { - margin-left: $spacer; - margin-right: $spacer; - } + display: block; + flex: 0 0 48%; + margin-bottom: $spacer; - li::before { - top: $spacer / 7; + @media (min-width: $screen-sm) { + flex-basis: 31%; + } + + &::before { + display: none; + } } } + img { + margin-bottom: 0; + } + a { - padding-top: $spacer / 6; - padding-bottom: $spacer / 6; + display: block; + + > div { + margin-bottom: 0; + } + + &:hover, + &:focus { + h4 { + color: $link-color; + } + } + } +} + +.empty { + padding-top: 15vh; + display: flex; + justify-content: center; +} + +.emptyMessage { + color: $brand-grey-light; +} + +.emptyMessageText { + margin-bottom: 0; + position: relative; + + &::after { + overflow: hidden; display: inline-block; + vertical-align: bottom; + animation: ellipsis steps(4, end) 1s infinite; + content: '\2026'; // ascii code for the ellipsis character + width: 0; + position: absolute; + left: 101%; + bottom: 0; } } @@ -48,3 +95,9 @@ opacity: 1; } } + +@keyframes ellipsis { + to { + width: 1rem; + } +} diff --git a/src/components/atoms/Image.jsx b/src/components/atoms/Image.jsx index aa34f98e..fffd4a07 100644 --- a/src/components/atoms/Image.jsx +++ b/src/components/atoms/Image.jsx @@ -1,23 +1,29 @@ -import React from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import { graphql } from 'gatsby' import Img from 'gatsby-image' import styles from './Image.module.scss' -const Image = ({ fluid, fixed, alt }) => ( - {alt} -) +export default class Image extends PureComponent { + static propTypes = { + fluid: PropTypes.object, + fixed: PropTypes.object, + alt: PropTypes.string.isRequired + } -Image.propTypes = { - fluid: PropTypes.object, - fixed: PropTypes.object, - alt: PropTypes.string.isRequired + render() { + const { fluid, fixed, alt } = this.props + + return ( + {alt} + ) + } } export const imageSizeDefault = graphql` @@ -35,5 +41,3 @@ export const imageSizeThumb = graphql` } } ` - -export default Image diff --git a/src/components/atoms/Modal.module.scss b/src/components/atoms/Modal.module.scss index 38535960..76cddab7 100644 --- a/src/components/atoms/Modal.module.scss +++ b/src/components/atoms/Modal.module.scss @@ -9,8 +9,8 @@ right: 0; bottom: 0; z-index: 9; - background: rgba($body-background-color, .9); - // backdrop-filter: blur(5px); + background: rgba($body-background-color, .95); + backdrop-filter: blur(5px); animation: fadein .3s; padding: $spacer; @@ -65,10 +65,10 @@ overflow: hidden; // more cross-browser backdrop-filter - body > div:first-child { - transition: filter .85s ease-out; - filter: blur(5px); - } + // body > div:first-child { + // transition: filter .85s ease-out; + // filter: blur(5px); + // } } .modal__title { diff --git a/src/components/molecules/RelatedPosts.jsx b/src/components/molecules/RelatedPosts.jsx index 76e8c534..12e5dfe0 100644 --- a/src/components/molecules/RelatedPosts.jsx +++ b/src/components/molecules/RelatedPosts.jsx @@ -1,7 +1,7 @@ -import React, { Fragment, PureComponent } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { Link, graphql, StaticQuery } from 'gatsby' -import Image from '../atoms/Image' +import { graphql, StaticQuery } from 'gatsby' +import PostTeaser from '../Post/PostTeaser' import styles from './RelatedPosts.module.scss' const query = graphql` @@ -45,32 +45,6 @@ const postsWithDataFilter = (postsArray, key, valuesToFind) => { return newArray } -const PostItem = ({ post }) => { - return ( -
  • - - {post.node.frontmatter.image ? ( - - {post.node.frontmatter.title} -

    {post.node.frontmatter.title}

    -
    - ) : ( -
    -

    {post.node.frontmatter.title}

    -
    - )} - -
  • - ) -} - -PostItem.propTypes = { - post: PropTypes.object.isRequired -} - class RelatedPosts extends PureComponent { shufflePosts = () => { this.forceUpdate() @@ -95,8 +69,8 @@ class RelatedPosts extends PureComponent { {filteredPosts .sort(() => 0.5 - Math.random()) .slice(0, 6) - .map(post => ( - + .map(({ node }) => ( + ))}