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:
commit
ca1086010e
@ -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/.
|
|
@ -1,6 +0,0 @@
|
|||||||
All photos & image assets under `./content/media`, `./src/images`, and `assets sheet.psd` are plain ol' copyright.
|
|
||||||
|
|
||||||
Copyright (c) 2008–2018 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.
|
|
29
README.md
29
README.md
@ -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/">
|
[](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) 2008–2018 Matthias Kretschmann
|
Copyright (c) 2008–2018 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!
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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}>
|
<>
|
||||||
×
|
<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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
src/components/Search/SearchResultsEmpty.jsx
Normal file
24
src/components/Search/SearchResultsEmpty.jsx
Normal 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
|
34
src/components/Search/SearchResultsEmpty.module.scss
Normal file
34
src/components/Search/SearchResultsEmpty.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
|
@ -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%);
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user