1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-17 01:53:13 +02:00

new search results presentation

This commit is contained in:
Matthias Kretschmann 2018-11-18 19:34:55 +01:00
parent 2a95ffebb9
commit 1fea655105
Signed by: m
GPG Key ID: 606EEEF3C479A91F
13 changed files with 287 additions and 149 deletions

View File

@ -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

View File

@ -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 (
<li>
<Link to={post.fields.slug} onClick={toggleSearch && toggleSearch}>
{post.frontmatter.image ? (
<>
<Image
fluid={post.frontmatter.image.childImageSharp.fluid}
alt={post.frontmatter.title}
/>
<h4 className={styles.postTitle}>{post.frontmatter.title}</h4>
</>
) : (
<div className={styles.empty}>
<h4 className={styles.postTitle}>{post.frontmatter.title}</h4>
</div>
)}
</Link>
</li>
)
}
}

View File

@ -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;
}
}

View File

@ -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 {
<Helmet>
<body className="hasSearchOpen" />
</Helmet>
<CSSTransition
appear={searchOpen}
in={searchOpen}
@ -67,7 +70,12 @@ export default class Search extends PureComponent {
/>
</section>
</CSSTransition>
<SearchResults results={results} onClose={this.toggleSearch} />
<SearchResults
searchQuery={query}
results={results}
toggleSearch={this.toggleSearch}
/>
</>
)}
</>

View File

@ -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);
}
}

View File

@ -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 }) => (
<>
<Input
className={styles.searchInput}
type="search"
placeholder="Search everything"
autoFocus // eslint-disable-line
{...props}
/>
<button
className={styles.searchInputClose}
onClick={onToggle}
title="Close search"
>
&times;
</button>
</>
)
export default SearchInput
export default class SearchInput extends PureComponent {
render() {
return (
<>
<Input
className={styles.searchInput}
type="search"
placeholder="Search everything"
autoFocus // eslint-disable-line
{...this.props}
/>
<button
className={styles.searchInputClose}
onClick={this.props.onToggle}
title="Close search"
>
&times;
</button>
</>
)
}
}

View File

@ -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;

View File

@ -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(
<div className={styles.searchResults}>
<Container>
<ul>
{results.length > 0 &&
results.map(page => (
<li key={page.slug}>
<Link to={page.slug} onClick={onClose}>
{page.title}
</Link>
</li>
))}
</ul>
</Container>
</div>,
document.getElementById('document')
)
const SearchEmpty = ({ searchQuery, results }) => (
<div className={styles.empty}>
<header className={styles.emptyMessage}>
<p className={styles.emptyMessageText}>
{searchQuery.length > 1 && results.length === 0
? 'No results found'
: searchQuery.length === 1
? 'Just one more character'
: 'Awaiting your input'}
</p>
</header>
</div>
)
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 (
<StaticQuery
query={query}
render={data => {
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(
<div className={styles.searchResults}>
<Container>
{results.length > 0 ? (
<ul>
{results.map(page =>
posts
.filter(post => post.node.fields.slug === page.slug)
.map(({ node }) => (
<PostTeaser
key={page.slug}
post={node}
toggleSearch={toggleSearch}
/>
))
)}
</ul>
) : (
<SearchEmpty searchQuery={searchQuery} results={results} />
)}
</Container>
</div>,
document.getElementById('document')
)
}}
/>
)
}
}

View File

@ -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;
}
}

View File

@ -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 }) => (
<Img
className={styles.imageWrap}
backgroundColor="#dfe8ef"
fluid={fluid ? fluid : null}
fixed={fixed ? fixed : null}
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 (
<Img
className={styles.imageWrap}
backgroundColor="#dfe8ef"
fluid={fluid ? fluid : null}
fixed={fixed ? fixed : null}
alt={alt}
/>
)
}
}
export const imageSizeDefault = graphql`
@ -35,5 +41,3 @@ export const imageSizeThumb = graphql`
}
}
`
export default Image

View File

@ -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 {

View File

@ -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 (
<li>
<Link to={post.node.fields.slug}>
{post.node.frontmatter.image ? (
<Fragment>
<Image
fluid={post.node.frontmatter.image.childImageSharp.fluid}
alt={post.node.frontmatter.title}
/>
<h4 className={styles.postTitle}>{post.node.frontmatter.title}</h4>
</Fragment>
) : (
<div className={styles.empty}>
<h4 className={styles.postTitle}>{post.node.frontmatter.title}</h4>
</div>
)}
</Link>
</li>
)
}
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 => (
<PostItem key={post.node.id} post={post} />
.map(({ node }) => (
<PostTeaser key={node.id} post={node} />
))}
</ul>
<button

View File

@ -1,18 +1,6 @@
@import 'variables';
@import 'mixins';
.empty {
height: 100%;
min-height: 80px;
display: flex;
align-items: center;
padding: $spacer / 4;
h4 {
margin-top: 0;
}
}
.relatedPosts {
margin-top: -($spacer * 2);
margin-bottom: $spacer;
@ -65,22 +53,6 @@
font-size: $font-size-h3;
}
.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;
}
}
.button {
margin: auto;
display: block;