mirror of
https://github.com/kremalicious/blog.git
synced 2025-01-03 02:15:08 +01:00
fix search
This commit is contained in:
parent
3229db8143
commit
495aba8e96
@ -1,4 +1,6 @@
|
||||
plugins/gatsby-redirect-from
|
||||
node_modules
|
||||
public
|
||||
.cache
|
||||
node_modules/
|
||||
.cache/
|
||||
static/
|
||||
public/
|
||||
coverage/
|
@ -1,5 +0,0 @@
|
||||
node_modules/
|
||||
.cache/
|
||||
static/
|
||||
public/
|
||||
coverage/
|
@ -1,7 +1,7 @@
|
||||
dist: xenial
|
||||
language: node_js
|
||||
node_js:
|
||||
- '11'
|
||||
- '12'
|
||||
|
||||
git:
|
||||
depth: 10
|
||||
|
@ -15,7 +15,7 @@
|
||||
"rename:scrypt": "sed -i -e 's|./build/Release/scrypt|scrypt|g' node_modules/scrypt/index.js",
|
||||
"copy": "cp -R content/media/ public",
|
||||
"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:md": "markdownlint './**/*.{md,markdown}' --ignore './{node_modules,public,.cache,.git,coverage}/**/*'",
|
||||
"format": "npm run lint:js -- --fix && npm run lint:css -- --fix",
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ export default function SearchInput({
|
||||
}: {
|
||||
value: string
|
||||
onToggle(): void
|
||||
onChange(): void
|
||||
onChange(e: Event): void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { graphql, StaticQuery } from 'gatsby'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import Container from '../atoms/Container'
|
||||
import PostTeaser from '../Post/PostTeaser'
|
||||
import SearchResultsEmpty from './SearchResultsEmpty'
|
||||
@ -38,42 +38,33 @@ export default function SearchResults({
|
||||
results: any
|
||||
toggleSearch(): void
|
||||
}) {
|
||||
return (
|
||||
<StaticQuery
|
||||
query={query}
|
||||
render={data => {
|
||||
const posts = data.allMarkdownRemark.edges
|
||||
const data = useStaticQuery(query)
|
||||
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')
|
||||
)
|
||||
}}
|
||||
/>
|
||||
// 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 }: { node: any }) => (
|
||||
<PostTeaser
|
||||
key={page.slug}
|
||||
post={node}
|
||||
toggleSearch={toggleSearch}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<SearchResultsEmpty searchQuery={searchQuery} results={results} />
|
||||
)}
|
||||
</Container>
|
||||
</div>,
|
||||
document.getElementById('document')
|
||||
)
|
||||
}
|
||||
|
45
src/components/Search/index.test.tsx
Normal file
45
src/components/Search/index.test.tsx
Normal 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'))
|
||||
})
|
||||
})
|
68
src/components/Search/index.tsx
Normal file
68
src/components/Search/index.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
import PostTeaser from '../Post/PostTeaser'
|
||||
import styles from './RelatedPosts.module.scss'
|
||||
@ -51,7 +51,13 @@ const postsWithDataFilter = (
|
||||
export default function RelatedPosts({ tags }: { tags: string[] }) {
|
||||
const data = useStaticQuery(query)
|
||||
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 (
|
||||
<aside className={styles.relatedPosts}>
|
||||
@ -64,7 +70,9 @@ export default function RelatedPosts({ tags }: { tags: string[] }) {
|
||||
<PostTeaser key={node.id} post={node} />
|
||||
))}
|
||||
</ul>
|
||||
<button className={`${styles.button} btn`}>Refresh Related Posts</button>
|
||||
<button className={`${styles.button} btn`} onClick={refreshPosts}>
|
||||
Refresh Related Posts
|
||||
</button>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
@ -10,9 +10,45 @@ import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
|
||||
import styles from './Footer.module.scss'
|
||||
import { useSiteMetadata } from '../../hooks/use-site-metadata'
|
||||
|
||||
export default function Footer() {
|
||||
const { name, uri, bitcoin, github } = useSiteMetadata()
|
||||
function Copyright({
|
||||
toggleModal,
|
||||
showModal
|
||||
}: {
|
||||
toggleModal(): void
|
||||
showModal: boolean
|
||||
}) {
|
||||
const { name, uri, bitcoin, github } = useSiteMetadata().author
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
return (
|
||||
<section className={styles.copyright}>
|
||||
<p>
|
||||
© 2005–
|
||||
{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 toggleModal = () => {
|
||||
@ -24,31 +60,7 @@ export default function Footer() {
|
||||
<Container>
|
||||
<Vcard />
|
||||
<Subscribe />
|
||||
|
||||
<section className={styles.copyright}>
|
||||
<p>
|
||||
© 2005–
|
||||
{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>
|
||||
<Copyright showModal={showModal} toggleModal={toggleModal} />
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import Container from '../atoms/Container'
|
||||
import Search from '../Search/Search'
|
||||
import Search from '../Search'
|
||||
import Menu from '../molecules/Menu'
|
||||
|
||||
import styles from './Header.module.scss'
|
||||
|
Loading…
Reference in New Issue
Block a user