Working implementation of text search

This commit is contained in:
LordMathis 2020-01-11 15:51:14 +01:00
parent d5eab71f3a
commit e2be492667
No known key found for this signature in database
GPG Key ID: 575849FD91CE470C
8 changed files with 131 additions and 24 deletions

View File

@ -18,6 +18,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"async": "^3.1.0", "async": "^3.1.0",
"axios": "^0.19.1",
"chokidar": "^3.3.0", "chokidar": "^3.3.0",
"express": "^4.13.4", "express": "^4.13.4",
"express-static-gzip": "^2.0.5", "express-static-gzip": "^2.0.5",

View File

@ -10,8 +10,13 @@ export default class Blog extends Component {
static propTypes = { static propTypes = {
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
posts: PropTypes.arrayOf(PropTypes.object).isRequired, posts: PropTypes.arrayOf(PropTypes.object).isRequired,
searchString: PropTypes.string.isRequired, searchString: PropTypes.string,
handleChange: PropTypes.func.isRequired expanded: PropTypes.bool.isRequired,
handleChange: PropTypes.func.isRequired,
handleFocus: PropTypes.func.isRequired,
handleBlur: PropTypes.func.isRequired,
handleEnter: PropTypes.func.isRequired,
handleSearch: PropTypes.func.isRequired
} }
render () { render () {
@ -19,8 +24,17 @@ export default class Blog extends Component {
if (this.props.isLoading) { if (this.props.isLoading) {
return ( return (
<div className={contentStyle.contentWrapper} id="blog"> <div className={`${contentStyle.content}`} id="blog" role="region" aria-label="Blog posts">
<Spinner/> <div className={styles.headerContainer}>
<Header header={'Blog'} role="heading" aria-level="2"/>
<SearchBox searchString={this.props.searchString}
expanded={this.props.expanded}
handleChange={this.props.handleChange}
handleFocus={this.props.handleFocus}
handleBlur={this.props.handleBlur}
handleSearch={this.props.handleSearch} />
</div>
<Spinner />
</div> </div>
) )
} }
@ -28,7 +42,7 @@ export default class Blog extends Component {
const posts = this.props.posts.sort((a, b) => { const posts = this.props.posts.sort((a, b) => {
return new Date(b.published) - new Date(a.published) return new Date(b.published) - new Date(a.published)
}) })
const postsHTML = posts.map((post) => let postsHTML = posts.map((post) =>
<div key={post.title} className={styles.postListItem} role="listitem"> <div key={post.title} className={styles.postListItem} role="listitem">
<div className={styles.postHeader} > <div className={styles.postHeader} >
<a href={post.link} className={styles.postTitle}>{post.title}</a> <a href={post.link} className={styles.postTitle}>{post.title}</a>
@ -39,16 +53,29 @@ export default class Blog extends Component {
</div> </div>
) )
if (postsHTML.length < 1) {
postsHTML = (
<div>
<span>No posts found</span>
</div>
)
}
return ( return (
<div className={`${contentStyle.content}`} id="blog" role="region" aria-label="Blog posts"> <div className={`${contentStyle.content}`} id="blog" role="region" aria-label="Blog posts">
<div className={styles.headerContainer}> <div className={styles.headerContainer}>
<Header header={'Blog'} role="heading" aria-level="2"/> <Header header={'Blog'} role="heading" aria-level="2"/>
<SearchBox searchString={this.props.searchString} handleChange={this.props.handleChange} /> <SearchBox searchString={this.props.searchString}
expanded={this.props.expanded}
handleChange={this.props.handleChange}
handleFocus={this.props.handleFocus}
handleBlur={this.props.handleBlur}
handleEnter={this.props.handleEnter}
handleSearch={this.props.handleSearch} />
</div> </div>
<div className={`${styles.postsList}`} role="list"> <div className={`${styles.postsList}`} role="list">
{postsHTML} {postsHTML}
</div> </div>
</div> </div>
) )
} }

View File

@ -6,14 +6,26 @@ import styles from './SearchBox.scss'
export default class SearchBox extends Component { export default class SearchBox extends Component {
static propTypes = { static propTypes = {
handleChange: PropTypes.func.isRequired, handleChange: PropTypes.func.isRequired,
searchString: PropTypes.string handleFocus: PropTypes.func.isRequired,
handleBlur: PropTypes.func.isRequired,
handleEnter: PropTypes.func.isRequired,
handleSearch: PropTypes.func.isRequired,
searchString: PropTypes.string,
expanded: PropTypes.bool.isRequired
} }
render () { render () {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<input placeholder='Search' className={styles.search} type="text" value={this.props.searchString} onChange={this.props.handleChange}/> <input placeholder='Search'
<span> className={`${styles.search} ${this.props.expanded ? styles.expanded : ''}`}
type="text"
value={this.props.searchString}
onChange={this.props.handleChange}
onFocus={this.props.handleFocus}
onBlur={this.props.handleBlur}
onKeyDown={this.props.handleEnter} />
<span onClick={this.props.handleSearch} >
<i className={`fa fa-search ${styles.icon}`}></i> <i className={`fa fa-search ${styles.icon}`}></i>
</span> </span>
</div> </div>

View File

@ -21,8 +21,10 @@
transition: width 0.4s ease; transition: width 0.4s ease;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
}
&:focus{ width: 300px; } .expanded {
width: 300px;
} }
.icon{ .icon{

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Blog } from '../components' import { Blog } from '../components'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import axios from 'axios'
import '../stylesheets/globals.scss' import '../stylesheets/globals.scss'
export default class BlogContainer extends Component { export default class BlogContainer extends Component {
@ -13,7 +14,8 @@ export default class BlogContainer extends Component {
this.state = { this.state = {
isLoading: false, isLoading: false,
posts: props.posts, posts: props.posts,
searchString: '' searchString: '',
expanded: false
} }
} }
@ -21,9 +23,53 @@ export default class BlogContainer extends Component {
this.setState({ searchString: event.target.value }) this.setState({ searchString: event.target.value })
} }
handleFocus () {
this.setState({ expanded: true })
}
handleBlur () {
this.setState({ expanded: false })
}
handleEnter (event) {
if (event.key === 'Enter') {
this.handleSearch()
}
}
handleSearch () {
if (this.state.expanded && this.state.searchString) {
this.search()
} else {
this.setState({ expanded: true })
}
}
search () {
this.setState({
isLoading: true
}, () => {
axios.get(`/api/v1/posts?search=${this.state.searchString}`)
.then((data) => {
this.setState({
isLoading: false,
posts: data.data
})
})
})
}
render () { render () {
return ( return (
<Blog isLoading={ this.state.isLoading } posts={ this.state.posts } searchString={this.state.searchString} handleChange={this.handleChange.bind(this)} /> <Blog isLoading={ this.state.isLoading }
posts={ this.state.posts }
searchString={this.state.searchString}
expanded={this.state.expanded}
handleChange={this.handleChange.bind(this)}
handleFocus={this.handleFocus.bind(this)}
handleBlur={this.handleBlur.bind(this)}
handleEnter={this.handleEnter.bind(this)}
handleSearch={this.handleSearch.bind(this)} />
) )
} }
} }

View File

@ -16,3 +16,7 @@
font-family: $font-header; font-family: $font-header;
} }
} }
@-ms-viewport{
width: device-width;
}

View File

@ -50,15 +50,16 @@ function renderFullPage (html, head, data, config) {
<html lang="en"> <html lang="en">
<head> <head>
<title>${config.title}</title> <title>${config.title}</title>
<!-- Google Fonts --> <meta name="viewport" content="width=device-width">
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:700&amp;subset=latin-ext" rel="stylesheet" rel="preload"> <!-- Google Fonts -->
<!-- Font Awesome --> <link href="https://fonts.googleapis.com/css?family=Open+Sans|Open+Sans+Condensed:700&amp;subset=latin-ext" rel="stylesheet" rel="preload">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" rel="preload" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous"> <!-- Font Awesome -->
<!-- Stylesheet --> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet" rel="preload" integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1" crossorigin="anonymous">
<link href=${manifest['bundle.css']} rel="stylesheet" rel="preload"> <!-- Stylesheet -->
<!-- Initial Data --> <link href=${manifest['bundle.css']} rel="stylesheet" rel="preload">
<script>window.__INITIAL_DATA__ = ${serialize(initialData)}</script> <!-- Initial Data -->
${head.scripts.join('\n')} <script>window.__INITIAL_DATA__ = ${serialize(initialData)}</script>
${head.scripts.join('\n')}
</head> </head>
<body> <body>
<div id="root">${html}</div> <div id="root">${html}</div>

View File

@ -1222,6 +1222,13 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c"
integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A== integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==
axios@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.1.tgz#8a6a04eed23dfe72747e1dd43c604b8f1677b5aa"
integrity sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==
dependencies:
follow-redirects "1.5.10"
babel-eslint@^10.0.1: babel-eslint@^10.0.1:
version "10.0.3" version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
@ -2352,7 +2359,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
debug@3.1.0: debug@3.1.0, debug@=3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
@ -3228,6 +3235,13 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.3.6" readable-stream "^2.3.6"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
for-in@^1.0.2: for-in@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"