1
0
mirror of https://github.com/kremalicious/blog.git synced 2025-01-05 03:15:07 +01:00

fix search

This commit is contained in:
Matthias Kretschmann 2019-10-02 20:48:59 +02:00
parent 3229db8143
commit 495aba8e96
Signed by: m
GPG Key ID: 606EEEF3C479A91F
13 changed files with 200 additions and 166 deletions

View File

@ -1,4 +1,6 @@
plugins/gatsby-redirect-from plugins/gatsby-redirect-from
node_modules node_modules/
public .cache/
.cache static/
public/
coverage/

View File

@ -1,5 +0,0 @@
node_modules/
.cache/
static/
public/
coverage/

View File

@ -1,7 +1,7 @@
dist: xenial dist: xenial
language: node_js language: node_js
node_js: node_js:
- '11' - '12'
git: git:
depth: 10 depth: 10

View File

@ -15,7 +15,7 @@
"rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js", "rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js",
"copy": "cp -R content/media/ public", "copy": "cp -R content/media/ public",
"lint": "run-p --continue-on-error lint:js lint:css lint:md", "lint": "run-p --continue-on-error lint:js lint:css lint:md",
"lint:js": "eslint --ignore-path .gitignore --ignore-path .prettierignore --ext .js,.jsx,.ts,.tsx .", "lint:js": "eslint --ignore-path .gitignore --ext .js,.jsx,.ts,.tsx .",
"lint:css": "stylelint 'src/**/*.{css,scss}'", "lint:css": "stylelint 'src/**/*.{css,scss}'",
"lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git,coverage}/**/*'", "lint:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git,coverage}/**/*'",
"format": "npm run lint:js -- --fix && npm run lint:css -- --fix", "format": "npm run lint:js -- --fix && npm run lint:css -- --fix",

View File

@ -1,87 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Helmet from 'react-helmet'
import { CSSTransition } from 'react-transition-group'
import SearchInput from './SearchInput'
import SearchButton from './SearchButton'
import SearchResults from './SearchResults'
import styles from './Search.module.scss'
export default class Search extends PureComponent<
{},
{ searchOpen: boolean; query: string; results: string[] }
> {
state = {
searchOpen: false,
query: '',
results: []
}
static propTypes = {
lng: PropTypes.string.isRequired
}
toggleSearch = () => {
this.setState(prevState => ({
searchOpen: !prevState.searchOpen
}))
}
getSearchResults(query: string) {
if (!query || !window.__LUNR__) return []
const lunrIndex = window.__LUNR__[this.props.lng]
const results = lunrIndex.index.search(query)
return results.map(({ ref }) => lunrIndex.store[ref])
}
search = (event: any) => {
const query = event.target.value
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
const results = query.length > 1 ? this.getSearchResults(`${query}*`) : []
this.setState({
results,
query
})
}
render() {
const { searchOpen, query, results } = this.state
return (
<>
<SearchButton onClick={this.toggleSearch} />
{searchOpen && (
<>
<Helmet>
<body className="hasSearchOpen" />
</Helmet>
<CSSTransition
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}
/>
</>
)}
</>
)
}
}

View File

@ -9,7 +9,7 @@ export default function SearchInput({
}: { }: {
value: string value: string
onToggle(): void onToggle(): void
onChange(): void onChange(e: Event): void
}) { }) {
return ( return (
<> <>

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { graphql, StaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import Container from '../atoms/Container' import Container from '../atoms/Container'
import PostTeaser from '../Post/PostTeaser' import PostTeaser from '../Post/PostTeaser'
import SearchResultsEmpty from './SearchResultsEmpty' import SearchResultsEmpty from './SearchResultsEmpty'
@ -38,42 +38,33 @@ export default function SearchResults({
results: any results: any
toggleSearch(): void toggleSearch(): void
}) { }) {
return ( const data = useStaticQuery(query)
<StaticQuery const posts = data.allMarkdownRemark.edges
query={query}
render={data => {
const posts = data.allMarkdownRemark.edges
// creating portal to break out of DOM node we're in // creating portal to break out of DOM node we're in
// and render the results in content container // and render the results in content container
return ReactDOM.createPortal( return ReactDOM.createPortal(
<div className={styles.searchResults}> <div className={styles.searchResults}>
<Container> <Container>
{results.length > 0 ? ( {results.length > 0 ? (
<ul> <ul>
{results.map(page => {results.map(page =>
posts posts
.filter(post => post.node.fields.slug === page.slug) .filter(post => post.node.fields.slug === page.slug)
.map(({ node }) => ( .map(({ node }: { node: any }) => (
<PostTeaser <PostTeaser
key={page.slug} key={page.slug}
post={node} post={node}
toggleSearch={toggleSearch} toggleSearch={toggleSearch}
/> />
)) ))
)} )}
</ul> </ul>
) : ( ) : (
<SearchResultsEmpty <SearchResultsEmpty searchQuery={searchQuery} results={results} />
searchQuery={searchQuery} )}
results={results} </Container>
/> </div>,
)} document.getElementById('document')
</Container>
</div>,
document.getElementById('document')
)
}}
/>
) )
} }

View File

@ -0,0 +1,45 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Search from '.'
import { useStaticQuery } from 'gatsby'
describe('Search', () => {
beforeEach(() => {
useStaticQuery.mockImplementation(() => {
return {
allMarkdownRemark: {
edges: [
{
node: {
id: 'ddd',
frontmatter: {
title: 'Hello',
image: {
childImageSharp: 'hello'
}
},
fields: {
slug: '/hello/'
}
}
}
]
}
}
})
const portalRoot = document.createElement('div')
portalRoot.setAttribute('id', 'document')
document.body.appendChild(portalRoot)
})
it('can be opened', () => {
const { getByTitle, getByPlaceholderText } = render(<Search lng="en" />)
fireEvent.click(getByTitle('Search'))
fireEvent.change(getByPlaceholderText('Search everything'), {
target: { value: 'hello' }
})
fireEvent.click(getByTitle('Close search'))
})
})

View File

@ -0,0 +1,68 @@
import React, { useState } from 'react'
import Helmet from 'react-helmet'
import { CSSTransition } from 'react-transition-group'
import SearchInput from './SearchInput'
import SearchButton from './SearchButton'
import SearchResults from './SearchResults'
import styles from './index.module.scss'
function getSearchResults(query: string, lng: string) {
if (!query || !window.__LUNR__) return []
const lunrIndex = window.__LUNR__[lng]
const results = lunrIndex.index.search(query)
return results.map(({ ref }: { ref: string }) => lunrIndex.store[ref])
}
export default function Search({ lng }: { lng: string }) {
const [searchOpen, setSearchOpen] = useState(false)
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const toggleSearch = () => {
setSearchOpen(!searchOpen)
}
const search = (event: any) => {
const query = event.target.value
// wildcard search https://lunrjs.com/guides/searching.html#wildcards
const results = query.length > 1 ? getSearchResults(`${query}*`, lng) : []
setQuery(query)
setResults(results)
}
return (
<>
<SearchButton onClick={toggleSearch} />
{searchOpen && (
<>
<Helmet>
<body className="hasSearchOpen" />
</Helmet>
<CSSTransition
appear={searchOpen}
in={searchOpen}
timeout={200}
classNames={styles}
>
<section className={styles.search}>
<SearchInput
value={query}
onChange={(e: Event) => search(e)}
onToggle={toggleSearch}
/>
</section>
</CSSTransition>
<SearchResults
searchQuery={query}
results={results}
toggleSearch={toggleSearch}
/>
</>
)}
</>
)
}

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { useState } from 'react'
import { graphql, useStaticQuery } from 'gatsby' import { graphql, useStaticQuery } from 'gatsby'
import PostTeaser from '../Post/PostTeaser' import PostTeaser from '../Post/PostTeaser'
import styles from './RelatedPosts.module.scss' import styles from './RelatedPosts.module.scss'
@ -51,7 +51,13 @@ const postsWithDataFilter = (
export default function RelatedPosts({ tags }: { tags: string[] }) { export default function RelatedPosts({ tags }: { tags: string[] }) {
const data = useStaticQuery(query) const data = useStaticQuery(query)
const posts = data.allMarkdownRemark.edges const posts = data.allMarkdownRemark.edges
const filteredPosts = postsWithDataFilter(posts, 'tags', tags) const [filteredPosts, setFilteredPosts] = useState(
postsWithDataFilter(posts, 'tags', tags)
)
function refreshPosts() {
setFilteredPosts(postsWithDataFilter(posts, 'tags', tags))
}
return ( return (
<aside className={styles.relatedPosts}> <aside className={styles.relatedPosts}>
@ -64,7 +70,9 @@ export default function RelatedPosts({ tags }: { tags: string[] }) {
<PostTeaser key={node.id} post={node} /> <PostTeaser key={node.id} post={node} />
))} ))}
</ul> </ul>
<button className={`${styles.button} btn`}>Refresh Related Posts</button> <button className={`${styles.button} btn`} onClick={refreshPosts}>
Refresh Related Posts
</button>
</aside> </aside>
) )
} }

View File

@ -10,9 +10,45 @@ import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
import styles from './Footer.module.scss' import styles from './Footer.module.scss'
import { useSiteMetadata } from '../../hooks/use-site-metadata' import { useSiteMetadata } from '../../hooks/use-site-metadata'
export default function Footer() { function Copyright({
const { name, uri, bitcoin, github } = useSiteMetadata() toggleModal,
showModal
}: {
toggleModal(): void
showModal: boolean
}) {
const { name, uri, bitcoin, github } = useSiteMetadata().author
const year = new Date().getFullYear() const year = new Date().getFullYear()
return (
<section className={styles.copyright}>
<p>
&copy; 2005&ndash;
{year + ' '}
<a href={uri} rel="me">
{name}
</a>
</p>
<p>
<a href={`${github}/blog`}>
<Github />
View source
</a>
<button className={styles.btc} onClick={toggleModal}>
<Bitcoin />
<code>{bitcoin}</code>
</button>
</p>
{showModal && (
<ModalThanks isOpen={showModal} handleCloseModal={toggleModal} />
)}
</section>
)
}
export default function Footer() {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const toggleModal = () => { const toggleModal = () => {
@ -24,31 +60,7 @@ export default function Footer() {
<Container> <Container>
<Vcard /> <Vcard />
<Subscribe /> <Subscribe />
<Copyright showModal={showModal} toggleModal={toggleModal} />
<section className={styles.copyright}>
<p>
&copy; 2005&ndash;
{year + ' '}
<a href={uri} rel="me">
{name}
</a>
</p>
<p>
<a href={`${github}/blog`}>
<Github />
View source
</a>
<button className={styles.btc} onClick={toggleModal}>
<Bitcoin />
<code>{bitcoin}</code>
</button>
</p>
{showModal && (
<ModalThanks isOpen={showModal} handleCloseModal={toggleModal} />
)}
</section>
</Container> </Container>
</footer> </footer>
) )

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { Link } from 'gatsby' import { Link } from 'gatsby'
import Container from '../atoms/Container' import Container from '../atoms/Container'
import Search from '../Search/Search' import Search from '../Search'
import Menu from '../molecules/Menu' import Menu from '../molecules/Menu'
import styles from './Header.module.scss' import styles from './Header.module.scss'