1
0
mirror of https://github.com/kremalicious/blog.git synced 2025-02-14 21:10:25 +01:00

Merge pull request #103 from kremalicious/feature/search-tweaks

new search results & search fixes
This commit is contained in:
Matthias Kretschmann 2018-11-18 20:09:40 +01:00 committed by GitHub
commit ca1086010e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 388 additions and 185 deletions

View File

@ -1,3 +0,0 @@
All post content under `./content/posts` is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
http://creativecommons.org/licenses/by-nc-sa/4.0/.

View File

@ -1,6 +0,0 @@
All photos & image assets under `./content/media`, `./src/images`, and `assets sheet.psd` are plain ol' copyright.
Copyright (c) 20082018 Matthias Kretschmann
Don't care if you fork & play with it, but you're not allowed to publish
anything from it as a whole without my written permission.

View File

@ -19,6 +19,7 @@
- [🎉 Features](#-features) - [🎉 Features](#-features)
- [🎆 EXIF extraction](#-exif-extraction) - [🎆 EXIF extraction](#-exif-extraction)
- [💰 Cryptocurrency donation via Web3/MetaMask](#-cryptocurrency-donation-via-web3metamask) - [💰 Cryptocurrency donation via Web3/MetaMask](#-cryptocurrency-donation-via-web3metamask)
- [🔍 Search](#-search)
- [🕸 Related Posts](#-related-posts) - [🕸 Related Posts](#-related-posts)
- [🐝 Coinhive](#-coinhive) - [🐝 Coinhive](#-coinhive)
- [🏆 SEO component](#-seo-component) - [🏆 SEO component](#-seo-component)
@ -61,7 +62,7 @@ Lets visitors say thanks with Bitcoin or Ether. Uses [web3.js](https://github.co
As a fallback, QR codes are generated with [react-qr-svg](https://github.com/no23reason/react-qr-svg) from the addresses defined in [`config.js`](config.js). As a fallback, QR codes are generated with [react-qr-svg](https://github.com/no23reason/react-qr-svg) from the addresses defined in [`config.js`](config.js).
<img width="1082" alt="screen shot 2018-10-14 at 22 03 57" src="https://user-images.githubusercontent.com/90316/46921544-1a512080-cffd-11e8-919f-d3e86dbd5cc5.png" /> <img width="700" alt="screen shot 2018-10-14 at 22 03 57" src="https://user-images.githubusercontent.com/90316/46921544-1a512080-cffd-11e8-919f-d3e86dbd5cc5.png" />
If you want to know how this works, have a look at the respective components under If you want to know how this works, have a look at the respective components under
@ -73,11 +74,23 @@ If you want to know how this works, have a look at the respective components und
- [`src/components/Web3Donation/utils.jsx`](src/components/Web3Donation/utils.jsx) - [`src/components/Web3Donation/utils.jsx`](src/components/Web3Donation/utils.jsx)
- [`src/components/atoms/Qr.jsx`](src/components/atoms/Qr.jsx) - [`src/components/atoms/Qr.jsx`](src/components/atoms/Qr.jsx)
### 🔍 Search
A global search is provided with [gatsby-plugin-lunr](https://github.com/humanseelabs/gatsby-plugin-lunr). That plugin creates a [Lunr](https://lunrjs.com) search index file of all posts on build time which is then queried against when the search field is used.
<img width="700" alt="screen shot 2018-11-18 at 19 44 30" src="https://user-images.githubusercontent.com/90316/48676679-634f4400-eb6a-11e8-936d-293505d5c5d9.png">
If you want to know how this works, have a look at the respective components under
- [`src/components/Search/Search.jsx`](src/components/Search/Search.jsx)
- [`src/components/Search/SearchResults.jsx`](src/components/Search/SearchResults.jsx)
- more in [`src/components/Search/`](src/components/Search/)
### 🕸 Related Posts ### 🕸 Related Posts
Under each post a list of related posts is displayed which are based on the tags of the currently viewed post. Also allows loading more related posts in place. Under each post a list of related posts is displayed which are based on the tags of the currently viewed post. Also allows loading more related posts in place.
<img width="691" alt="screen shot 2018-10-11 at 21 03 03" src="https://user-images.githubusercontent.com/90316/46827531-14f39c00-cd99-11e8-84aa-0e851c32c89c.png" /> <img width="700" alt="screen shot 2018-10-11 at 21 03 03" src="https://user-images.githubusercontent.com/90316/46827531-14f39c00-cd99-11e8-84aa-0e851c32c89c.png" />
If you want to know how this works, have a look at the respective component under If you want to know how this works, have a look at the respective component under
@ -198,20 +211,18 @@ The deploymeng script can be used locally too, the branch checks are only happen
The MIT License (MIT) The MIT License (MIT)
except for: EXCEPT FOR:
### Posts ### Posts
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"> [![Creative Commons License](https://i.creativecommons.org/l/by-nc-sa/4.0/80x15.png)](http://creativecommons.org/licenses/by-nc-sa/4.0/)
<img alt="Creative Commons License" style="border-width:0;" src="https://i.creativecommons.org/l/by-nc-sa/4.0/80x15.png" />
</a>
All post content under `./content/posts` is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</a>. All post content under `./content/posts` is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/).
### Photos & images ### Photos & images
All photos & image assets under `./content/media`, `./src/images`, and `assets sheet.psd` are plain ol' copyright. All photos & image assets are plain ol' copyright.
Copyright (c) 20082018 Matthias Kretschmann Copyright (c) 20082018 Matthias Kretschmann
Don't care if you fork & play with it, but you're not allowed to publish anything from it as a whole without my written permission. Don't care if you fork & play with it, but you're not allowed to publish anything from it as a whole without my written permission. Also please be aware, the combination of typography, colors & layout makes up my brand identity. So please don't just clone everything, but rather do a remix!

View File

@ -99,9 +99,10 @@ module.exports = {
// Fields to index. If store === true value will be stored in index file. // 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 // Attributes for custom indexing logic. See https://lunrjs.com/docs/lunr.Builder.html for details
fields: [ fields: [
{ name: 'title', store: true, attributes: { boost: 20 } }, { name: 'title', attributes: { boost: 20 } },
{ name: 'tags', attributes: { boost: 15 } },
{ name: 'excerpt', attributes: { boost: 10 } }, { name: 'excerpt', attributes: { boost: 10 } },
{ name: 'tags', store: true, attributes: { boost: 5 } }, { name: 'slug', store: true },
{ name: 'content' } { name: 'content' }
], ],
// How to resolve each field's value for a supported node type // How to resolve each field's value for a supported node type
@ -111,7 +112,8 @@ module.exports = {
title: node => node.frontmatter.title, title: node => node.frontmatter.title,
excerpt: node => node.excerpt, excerpt: node => node.excerpt,
tags: node => node.frontmatter.tags, tags: node => node.frontmatter.tags,
content: node => node.rawMarkdownBody content: node => node.rawMarkdownBody,
slug: node => node.fields.slug
} }
} }
} }

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

@ -1,4 +1,4 @@
import React, { PureComponent, Fragment } from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Helmet from 'react-helmet' import Helmet from 'react-helmet'
import { CSSTransition } from 'react-transition-group' import { CSSTransition } from 'react-transition-group'
@ -25,16 +25,6 @@ export default class Search extends PureComponent {
})) }))
} }
closeSearch = () => {
this.setState({
searchOpen: false,
query: '',
results: []
})
}
isSearchOpen = () => this.state.searchOpen === true
getSearchResults(query) { getSearchResults(query) {
if (!query || !window.__LUNR__) return [] if (!query || !window.__LUNR__) return []
const lunrIndex = window.__LUNR__[this.props.lng] const lunrIndex = window.__LUNR__[this.props.lng]
@ -44,7 +34,9 @@ export default class Search extends PureComponent {
search = event => { search = event => {
const query = event.target.value 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({ this.setState({
results, results,
query query
@ -55,34 +47,38 @@ export default class Search extends PureComponent {
const { searchOpen, query, results } = this.state const { searchOpen, query, results } = this.state
return ( return (
<Fragment> <>
<Helmet>
<body className={this.isSearchOpen() ? 'has-search-open' : null} />
</Helmet>
<SearchButton onClick={this.toggleSearch} /> <SearchButton onClick={this.toggleSearch} />
{searchOpen && ( {searchOpen && (
<CSSTransition <>
appear={searchOpen} <Helmet>
in={searchOpen} <body className="hasSearchOpen" />
timeout={200} </Helmet>
classNames={styles}
>
<section className={styles.search}>
<SearchInput
value={query}
onChange={this.search}
onToggle={this.closeSearch}
/>
</section>
</CSSTransition>
)}
{query && ( <CSSTransition
<SearchResults results={results} onClose={this.closeSearch} /> appear={searchOpen}
in={searchOpen}
timeout={200}
classNames={styles}
>
<section className={styles.search}>
<SearchInput
value={query}
onChange={this.search}
onToggle={this.toggleSearch}
/>
</section>
</CSSTransition>
<SearchResults
searchQuery={query}
results={results}
toggleSearch={this.toggleSearch}
/>
</>
)} )}
</Fragment> </>
) )
} }
} }

View File

@ -4,7 +4,7 @@
position: absolute; position: absolute;
left: $spacer / 2; left: $spacer / 2;
right: $spacer / 2; right: $spacer / 2;
top: 0; top: -($spacer / 4);
z-index: 10; z-index: 10;
input { input {
@ -40,3 +40,7 @@
transform: translate3d(0, -100px, 0); transform: translate3d(0, -100px, 0);
} }
} }
:global(.hasSearchOpen) {
overflow: hidden;
}

View File

@ -1,14 +1,26 @@
import React, { Fragment } from 'react' import React, { PureComponent } from 'react'
import Input from '../atoms/Input' import Input from '../atoms/Input'
import styles from './SearchInput.module.scss' import styles from './SearchInput.module.scss'
const SearchInput = props => ( export default class SearchInput extends PureComponent {
<Fragment> render() {
<Input type="search" placeholder="Search everything" {...props} /> return (
<button className={styles.searchInputClose} onClick={props.onToggle}> <>
&times; <Input
</button> className={styles.searchInput}
</Fragment> type="search"
) placeholder="Search everything"
autoFocus // eslint-disable-line
export default SearchInput {...this.props}
/>
<button
className={styles.searchInputClose}
onClick={this.props.onToggle}
title="Close search"
>
&times;
</button>
</>
)
}
}

View File

@ -1,7 +1,27 @@
@import 'variables'; @import 'variables';
.searchInput {
composes: input from '../atoms/Input.module.scss';
background: $input-bg-focus;
&::-webkit-search-cancel-button {
display: none;
}
&:hover {
background: $input-bg-focus;
}
}
.searchInputClose { .searchInputClose {
position: absolute; position: absolute;
right: $spacer / 4; right: $spacer / 2;
top: $spacer / 4; top: $spacer / 5;
font-size: $font-size-h3;
color: $brand-grey-light;
&:hover,
&:focus {
color: $link-color;
}
} }

View File

@ -1,30 +1,82 @@
import React from 'react' import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Link } from 'gatsby' import { graphql, StaticQuery } from 'gatsby'
import Container from '../atoms/Container' import Container from '../atoms/Container'
import PostTeaser from '../Post/PostTeaser'
import SearchResultsEmpty from './SearchResultsEmpty'
import styles from './SearchResults.module.scss' import styles from './SearchResults.module.scss'
const SearchResults = ({ results, onClose }) => const query = graphql`
ReactDOM.createPortal( query {
<div className={styles.searchResults}> allMarkdownRemark {
<Container> edges {
<ul> node {
{results.map(page => ( id
<li key={page.url}> frontmatter {
<Link to={page.url} onClick={onClose}> title
{page.title} image {
</Link> childImageSharp {
</li> ...ImageFluidThumb
))} }
</ul> }
</Container> }
</div>, fields {
document.getElementById('document') slug
) }
}
}
}
}
`
SearchResults.propTypes = { export default class SearchResults extends PureComponent {
results: PropTypes.array.isRequired 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>
) : (
<SearchResultsEmpty
searchQuery={searchQuery}
results={results}
/>
)}
</Container>
</div>,
document.getElementById('document')
)
}}
/>
)
}
} }
export default SearchResults

View File

@ -8,26 +8,68 @@
z-index: 10; z-index: 10;
top: 0; top: 0;
bottom: 0; bottom: 0;
background: $body-background-color; background: rgba($body-background-color, .95);
backdrop-filter: blur(5px);
animation: fadein .3s;
overflow: scroll;
-webkit-overflow-scrolling: touch;
height: 91vh;
ul { ul {
@include breakoutviewport; @include breakoutviewport;
margin-top: $spacer; padding: $spacer $spacer / 2;
margin-bottom: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@media (min-width: $screen-md) {
padding-left: 0;
padding-right: 0;
}
li { li {
margin-left: $spacer; display: block;
margin-right: $spacer; flex: 0 0 48%;
} margin-bottom: $spacer;
li::before { @media (min-width: $screen-sm) {
top: $spacer / 7; flex-basis: 31%;
}
&::before {
display: none;
}
} }
} }
img {
margin-bottom: 0;
}
a { a {
padding-top: $spacer / 6; display: block;
padding-bottom: $spacer / 6;
display: inline-block; > div {
margin-bottom: 0;
}
&:hover,
&:focus {
h4 {
color: $link-color;
}
}
}
}
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
} }
} }

View File

@ -0,0 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './SearchResultsEmpty.module.scss'
const SearchResultsEmpty = ({ 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>
)
SearchResultsEmpty.propTypes = {
results: PropTypes.array.isRequired,
searchQuery: PropTypes.string.isRequired
}
export default SearchResultsEmpty

View File

@ -0,0 +1,34 @@
@import 'variables';
.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;
}
}
@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 PropTypes from 'prop-types'
import { graphql } from 'gatsby' import { graphql } from 'gatsby'
import Img from 'gatsby-image' import Img from 'gatsby-image'
import styles from './Image.module.scss' import styles from './Image.module.scss'
const Image = ({ fluid, fixed, alt }) => ( export default class Image extends PureComponent {
<Img static propTypes = {
className={styles.imageWrap} fluid: PropTypes.object,
backgroundColor="#dfe8ef" fixed: PropTypes.object,
fluid={fluid ? fluid : null} alt: PropTypes.string.isRequired
fixed={fixed ? fixed : null} }
alt={alt}
/>
)
Image.propTypes = { render() {
fluid: PropTypes.object, const { fluid, fixed, alt } = this.props
fixed: PropTypes.object,
alt: PropTypes.string.isRequired return (
<Img
className={styles.imageWrap}
backgroundColor="#dfe8ef"
fluid={fluid ? fluid : null}
fixed={fixed ? fixed : null}
alt={alt}
/>
)
}
} }
export const imageSizeDefault = graphql` export const imageSizeDefault = graphql`
@ -35,5 +41,3 @@ export const imageSizeThumb = graphql`
} }
} }
` `
export default Image

View File

@ -14,7 +14,7 @@
border-radius: $input-border-radius; border-radius: $input-border-radius;
box-shadow: none; box-shadow: none;
transition: all ease-in-out .15s; transition: all ease-in-out .15s;
-webkit-appearance: none; // screw you, iOS default inset box-shadow appearance: none;
&:hover { &:hover {
background: lighten($input-bg, 30%); background: lighten($input-bg, 30%);

View File

@ -9,8 +9,8 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 9; z-index: 9;
background: rgba($body-background-color, .9); background: rgba($body-background-color, .95);
// backdrop-filter: blur(5px); backdrop-filter: blur(5px);
animation: fadein .3s; animation: fadein .3s;
padding: $spacer; padding: $spacer;
@ -65,10 +65,10 @@
overflow: hidden; overflow: hidden;
// more cross-browser backdrop-filter // more cross-browser backdrop-filter
body > div:first-child { // body > div:first-child {
transition: filter .85s ease-out; // transition: filter .85s ease-out;
filter: blur(5px); // filter: blur(5px);
} // }
} }
.modal__title { .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 PropTypes from 'prop-types'
import { Link, graphql, StaticQuery } from 'gatsby' import { graphql, StaticQuery } from 'gatsby'
import Image from '../atoms/Image' import PostTeaser from '../Post/PostTeaser'
import styles from './RelatedPosts.module.scss' import styles from './RelatedPosts.module.scss'
const query = graphql` const query = graphql`
@ -45,32 +45,6 @@ const postsWithDataFilter = (postsArray, key, valuesToFind) => {
return newArray 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 { class RelatedPosts extends PureComponent {
shufflePosts = () => { shufflePosts = () => {
this.forceUpdate() this.forceUpdate()
@ -95,8 +69,8 @@ class RelatedPosts extends PureComponent {
{filteredPosts {filteredPosts
.sort(() => 0.5 - Math.random()) .sort(() => 0.5 - Math.random())
.slice(0, 6) .slice(0, 6)
.map(post => ( .map(({ node }) => (
<PostItem key={post.node.id} post={post} /> <PostTeaser key={node.id} post={node} />
))} ))}
</ul> </ul>
<button <button

View File

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