mirror of
https://github.com/kremalicious/blog.git
synced 2025-01-03 02:15:08 +01:00
new search results presentation
This commit is contained in:
parent
2a95ffebb9
commit
1fea655105
@ -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
|
||||
|
36
src/components/Post/PostTeaser.jsx
Normal file
36
src/components/Post/PostTeaser.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
29
src/components/Post/PostTeaser.module.scss
Normal file
29
src/components/Post/PostTeaser.module.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
>
|
||||
×
|
||||
</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"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user