Move archive to Whoosh and add search
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"moment": "^2.24.0",
|
||||
"query-string": "^6.8.3",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Link } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Route, Link, Switch } from 'react-router-dom';
|
||||
import './Style-light.css';
|
||||
import './Style-dark.css';
|
||||
import './fonts/Fonts.css';
|
||||
import Feed from './Feed.js';
|
||||
import Article from './Article.js';
|
||||
import Comments from './Comments.js';
|
||||
import Search from './Search.js';
|
||||
import Results from './Results.js';
|
||||
import ScrollToTop from './ScrollToTop.js';
|
||||
|
||||
class App extends React.Component {
|
||||
@@ -41,10 +43,15 @@ class App extends React.Component {
|
||||
<br />
|
||||
<span className='slogan'>Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode.</span>
|
||||
</p>
|
||||
<Route path='/(|search)' component={Search} />
|
||||
</div>
|
||||
|
||||
<Route path='/' exact component={Feed} />
|
||||
<Switch>
|
||||
<Route path='/search' component={Results} />
|
||||
<Route path='/:id' exact component={Article} />
|
||||
</Switch>
|
||||
<Route path='/:id/c' exact component={Comments} />
|
||||
<Route path='/:id' exact component={Article} />
|
||||
|
||||
<ScrollToTop />
|
||||
</Router>
|
||||
|
@@ -15,20 +15,20 @@ class Article extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount() {
|
||||
const id = this.props.match.params.id;
|
||||
|
||||
fetch('/api/' + id)
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
this.setState({ story: result.story });
|
||||
localStorage.setItem(id, JSON.stringify(result.story));
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
fetch('/api/' + id)
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
this.setState({ story: result.story });
|
||||
localStorage.setItem(id, JSON.stringify(result.story));
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@@ -17,25 +17,25 @@ class Article extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount() {
|
||||
const id = this.props.match.params.id;
|
||||
|
||||
fetch('/api/' + id)
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
localStorage.setItem(id, JSON.stringify(result.story));
|
||||
this.setState({ story: result.story }, () => {
|
||||
fetch('/api/' + id)
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
localStorage.setItem(id, JSON.stringify(result.story));
|
||||
this.setState({ story: result.story }, () => {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash) {
|
||||
document.getElementById(hash).scrollIntoView();
|
||||
}
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
displayComment(story, c, level) {
|
||||
|
@@ -14,14 +14,14 @@ class Feed extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch('/api')
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
this.setState({ stories: result.stories });
|
||||
componentDidMount() {
|
||||
fetch('/api')
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
this.setState({ stories: result.stories });
|
||||
clearStorage();
|
||||
localStorage.setItem('stories', JSON.stringify(result.stories));
|
||||
localStorage.setItem('stories', JSON.stringify(result.stories));
|
||||
result.stories.filter(x => x.score >= 20).slice(0, 25).forEach(x => {
|
||||
fetch('/api/' + x.id)
|
||||
.then(res => res.json())
|
||||
@@ -31,11 +31,11 @@ class Feed extends React.Component {
|
||||
}, error => {}
|
||||
);
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -46,13 +46,12 @@ class Feed extends React.Component {
|
||||
<div className='container'>
|
||||
<Helmet>
|
||||
<title>Feed - QotNews</title>
|
||||
<meta name="description" content="Reddit, Hacker News, and Tildes combined, then pre-rendered in reader mode" />
|
||||
</Helmet>
|
||||
{error && <p>Connection error?</p>}
|
||||
{stories ?
|
||||
<div>
|
||||
{stories.map((x, i) =>
|
||||
<div className='item'>
|
||||
<div className='item' key={i}>
|
||||
<div className='num'>
|
||||
{i+1}.
|
||||
</div>
|
||||
|
83
webclient/src/Results.js
Normal file
83
webclient/src/Results.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { siteLogo, sourceLink, infoLine } from './utils.js';
|
||||
|
||||
class Results extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
stories: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
performSearch = () => {
|
||||
const search = this.props.location.search;
|
||||
fetch('/api/search' + search)
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
(result) => {
|
||||
this.setState({ stories: result.results });
|
||||
},
|
||||
(error) => {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.performSearch();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location.search !== prevProps.location.search) {
|
||||
this.performSearch();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const stories = this.state.stories;
|
||||
const error = this.state.error;
|
||||
|
||||
return (
|
||||
<div className='container'>
|
||||
<Helmet>
|
||||
<title>Feed - QotNews</title>
|
||||
</Helmet>
|
||||
{error && <p>Connection error?</p>}
|
||||
{stories ?
|
||||
<div>
|
||||
{stories.length ?
|
||||
stories.map((x, i) =>
|
||||
<div className='item' key={i}>
|
||||
<div className='num'>
|
||||
{i+1}.
|
||||
</div>
|
||||
|
||||
<div className='title'>
|
||||
<Link className='link' to={'/' + x.id}>{siteLogo[x.source]} {x.title}</Link>
|
||||
|
||||
<span className='source'>
|
||||
​({sourceLink(x)})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{infoLine(x)}
|
||||
</div>
|
||||
)
|
||||
:
|
||||
<p>no results</p>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<p>loading...</p>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Results;
|
53
webclient/src/Search.js
Normal file
53
webclient/src/Search.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
|
||||
const getSearch = props => queryString.parse(props.location.search).q;
|
||||
|
||||
class Search extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {search: getSearch(this.props)};
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
searchArticles = (event) => {
|
||||
const search = event.target.value;
|
||||
this.setState({search: search});
|
||||
if (search.length >= 3) {
|
||||
const searchQuery = queryString.stringify({ 'q': search });
|
||||
this.props.history.replace('/search?' + searchQuery);
|
||||
} else {
|
||||
this.props.history.replace('/');
|
||||
}
|
||||
}
|
||||
|
||||
searchAgain = (event) => {
|
||||
event.preventDefault();
|
||||
const searchString = queryString.stringify({ 'q': event.target[0].value });
|
||||
this.props.history.push('/search?' + searchString);
|
||||
this.inputRef.current.blur();
|
||||
}
|
||||
|
||||
render() {
|
||||
const search = this.state.search;
|
||||
|
||||
return (
|
||||
<div className='search'>
|
||||
<div className='search-inside'>
|
||||
<form onSubmit={this.searchAgain}>
|
||||
<input
|
||||
placeholder='Search...'
|
||||
value={search}
|
||||
onChange={this.searchArticles}
|
||||
ref={this.inputRef}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Search);
|
@@ -6,6 +6,11 @@
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.dark input {
|
||||
color: #ddd;
|
||||
border: 1px solid #828282;
|
||||
}
|
||||
|
||||
.dark .item {
|
||||
color: #828282;
|
||||
}
|
||||
|
@@ -11,6 +11,14 @@ a {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1.05rem;
|
||||
background-color: transparent;
|
||||
border: 1px solid #828282;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 1rem auto;
|
||||
max-width: 64rem;
|
||||
|
@@ -7817,6 +7817,15 @@ qs@~6.5.2:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
|
||||
query-string@^6.8.3:
|
||||
version "6.8.3"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.8.3.tgz#fd9fb7ffb068b79062b43383685611ee47777d4b"
|
||||
integrity sha512-llcxWccnyaWlODe7A9hRjkvdCKamEKTh+wH8ITdTc3OhchaqUZteiSCX/2ablWHVrkVIe04dntnaZJ7BdyW0lQ==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
@@ -8878,6 +8887,11 @@ spdy@^4.0.0:
|
||||
select-hose "^2.0.0"
|
||||
spdy-transport "^3.0.0"
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@@ -8972,6 +8986,11 @@ stream-shift@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
|
||||
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
|
Reference in New Issue
Block a user