Browse Source

Favoriten-Funktion (#38)

* Add capability to mark site as favorite

* Add prop-types dependency

* Update test for SitesSearch

* New component SearchResultItem

* Rename readAll to getAll

* Add component FavoritesList

* Add FavoritesList to SiteSearch
main
Marian Steinbach 3 years ago committed by GitHub
parent
commit
4dac4f4af5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      package.json
  2. 79
      src/FavoritesList.js
  3. 14
      src/FavoritesList.test.js
  4. 65
      src/FavouriteAddRemove.js
  5. 37
      src/SearchResultItem.js
  6. 23
      src/SearchResultItem.test.js
  7. 7
      src/SiteDetailsPage.js
  8. 19
      src/SitesSearch.css
  9. 45
      src/SitesSearch.js
  10. 47
      src/SitesSearch.test.js
  11. 11
      src/index.css
  12. 62
      src/lib/Favorites.js
  13. 2
      yarn.lock

1
package.json

@ -38,6 +38,7 @@
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"promise": "^8.0.3",
"prop-types": "^15.7.2",
"punycode": "^2.1.1",
"raf": "^3.4.1",
"react": "^16.8.5",

79
src/FavoritesList.js

@ -0,0 +1,79 @@
import axios from 'axios';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SearchResultItem from './SearchResultItem';
class FavoritesList extends Component {
state = {
loading: true,
sites: [],
total: null,
};
componentDidMount() {
// load sites from URLs
let queryTermParts = this.props.urls.map((url) => {
return 'url:"' + url + '"';
});
let queryTerm = queryTermParts.join(' OR ');
axios.get('/api/v1/spider-results/query/?q=' + encodeURI(queryTerm))
.then((response) => {
if (response.data.hits.total > 0 && response.data.hits.hits.length > 0) {
this.setState({
loading: false,
sites: response.data.hits.hits,
total: response.data.hits.total,
});
}
})
.catch((err) => {
console.error(err);
});
}
render() {
if (this.state.loading) {
return (
<div className='row favs'>
<div className='col-12 text-center'>Lade Favoriten</div>
</div>
);
}
return (
<div className='row favs'>
<div className='col-12'>
<div className='row'>
<div className='col-12'>
<h2>Deine Favoriten</h2>
</div>
</div>
{
this.state.sites.map((site) => {
return <SearchResultItem key={site._source.url} site={site} />;
})
}
{
this.state.sites.length > this.props.sizeLimit ?
(
<div className='row'>
<div className='col-12 text-center truncate-info'>
Es werden nur {this.props.sizeLimit} Favoriten angezeigt.
</div>
</div>
)
: null
}
</div>
</div>
);
}
}
FavoritesList.propTypes = {
urls: PropTypes.array.isRequired,
sizeLimit: PropTypes.number.isRequired,
};
export default FavoritesList;

14
src/FavoritesList.test.js

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from "react-router-dom";
import FavoritesList from './FavoritesList';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(
<Router>
<FavoritesList sizeLimit={10} urls={['http://example.com/']}/>
</Router>,
div);
ReactDOM.unmountComponentAtNode(div);
});

65
src/FavouriteAddRemove.js

@ -0,0 +1,65 @@
import React, { Component } from 'react';
import { Favorites } from './lib/Favorites';
class FavouriteAddRemove extends Component {
state = {
isFav: false,
};
favs = new Favorites();
componentDidMount() {
if (this.favs.include(this.props.site.url)) {
this.setState({isFav: true});
}
}
onClickAdd = () => {
console.debug('onClickAdd');
this.favs.add(this.props.site.url);
this.setState({isFav: true});
};
onClickRemove = () => {
console.debug('onClickRemove');
this.favs.remove(this.props.site.url);
this.setState({isFav: false});
};
render() {
var fav = null;
if (this.state.isFav) {
fav = (
<div className='row'>
<div className='col-md-4' style={{marginBottom: 10}}>
<button type='button' className='btn btn-secondary' onClick={this.onClickRemove} style={{width: '100%'}}>Favorit entfernen</button>
</div>
<div className='col-md-8'>
<p>Die Seite ist als Favorit gespeichert und so immer über die Startseite verfügbar.</p>
</div>
</div>
);
} else {
fav = (
<div className='row'>
<div className='col-md-4' style={{marginBottom: 10}}>
<button type='button' className='btn btn-primary' onClick={this.onClickAdd} style={{width: '100%'}}>Zu meinen Favoriten</button>
</div>
<div className='col-md-8'>
<p>Speichere diese Seite als Favoriten, um sie direkt auf der Startseite angezeigt zu bekommen.</p>
</div>
</div>
);
}
return (
<div className='favourite-add-remove row'>
<div className='col-12'>
{fav}
</div>
</div>
);
}
}
export default FavouriteAddRemove;

37
src/SearchResultItem.js

@ -0,0 +1,37 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from "react-router-dom";
import LocationLabel from './LocationLabel';
import ScoreField from './ScoreField';
import URLField from './URLField';
class SearchResultItem extends Component {
render() {
return (
<Link key={this.props.site._source.url} to={`/sites/${ encodeURIComponent(this.props.site._source.url) }`} className='SitesSearch'>
<div className='SitesSearch row'>
<div className='col-9 col-sm-10 col-md-10'>
<LocationLabel
level={this.props.site._source.meta.level}
type={this.props.site._source.meta.type}
district={this.props.site._source.meta.district}
city={this.props.site._source.meta.city}
state={this.props.site._source.meta.state} truncate={true} />
<URLField url={this.props.site._source.url} link={false} />
</div>
<div className='col-3 col-sm-2 col-md-2 d-flex'>
<ScoreField score={this.props.site._source.score} maxScore={15} />
</div>
</div>
</Link>
);
}
}
SearchResultItem.propTypes = {
site: PropTypes.object.isRequired,
};
export default SearchResultItem;

23
src/SearchResultItem.test.js

@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from "react-router-dom";
import SearchResultItem from './SearchResultItem';
it('renders without crashing', () => {
const site = {
_source: {
url: 'http://example.com/',
meta: {
level: 'DE:ORTSVERBAND',
}
}
};
const div = document.createElement('div');
ReactDOM.render(
<Router>
<SearchResultItem site={site}/>
</Router>,
div);
ReactDOM.unmountComponentAtNode(div);
});

7
src/SiteDetailsPage.js

@ -1,7 +1,8 @@
import React, { Component } from 'react';
import { TypeField, StateField } from './LocationLabel';
import FavouriteAddRemove from './FavouriteAddRemove';
import LocationLabel from './LocationLabel';
import ScoreField from './ScoreField';
import { TypeField, StateField } from './LocationLabel';
import URLField from './URLField';
import './SiteDetailsPage.css';
import axios from 'axios';
@ -230,6 +231,10 @@ class SiteDetailsPage extends Component {
<hr />
<FavouriteAddRemove site={this.state.site} />
<hr />
<p><small>Site zuletzt geprüft am { new Date(this.state.site.created).toLocaleDateString('de-DE') }</small></p>
</div>

19
src/SitesSearch.css

@ -74,4 +74,21 @@ input[type="search"]::-webkit-search-cancel-button {
.row.results {
margin-top: 1rem;
}
}
.row.favs {
background-color: #edf2e4;
padding: 20px 0;
}
.row.favs h2 {
font-family: Arvo, sans-serif;
padding-left: 14px;
padding-bottom: 5px;
font-size: 24px;
color: #333;
}
.row.favs .truncate-info {
margin-top: 20px;
font-size: 0.9em;
}

45
src/SitesSearch.js

@ -1,17 +1,18 @@
import { Favorites } from './lib/Favorites';
import FavoritesList from './FavoritesList';
import axios from 'axios';
import React, { Component } from 'react';
import { Link } from "react-router-dom";
import LocationLabel from './LocationLabel';
import SearchForm from './SearchForm';
import ScoreField from './ScoreField';
import URLField from './URLField';
import SearchResultItem from './SearchResultItem';
import './SitesSearch.css';
import history from './history';
import InfiniteScroll from 'react-infinite-scroller';
import PropTypes from 'prop-types';
class SitesSearch extends Component {
itemsPerPage = 20;
favs = new Favorites();
state = {
loading: false,
@ -20,6 +21,7 @@ class SitesSearch extends Component {
userQuery: '',
hits: 0,
pageLoaded: null,
favorites: [],
};
componentDidMount() {
@ -31,6 +33,12 @@ class SitesSearch extends Component {
this.doSearch(q);
}
}
// load favorites
let fav = this.favs.getAll();
if (fav.length > 0) {
this.setState({favorites: fav});
}
}
/**
@ -115,21 +123,7 @@ class SitesSearch extends Component {
if (this.state.searchResultItems.length > 0) {
this.state.searchResultItems.forEach((site) => {
var row = (
<Link key={site._source.url} to={`/sites/${ encodeURIComponent(site._source.url) }`} className='SitesSearch'>
<div className='SitesSearch row'>
<div className='col-9 col-sm-10 col-md-10'>
<LocationLabel level={site._source.meta.level} type={site._source.meta.type} district={site._source.meta.district} city={site._source.meta.city} state={site._source.meta.state} truncate={true} />
<URLField url={site._source.url} link={false} />
</div>
<div className='col-3 col-sm-2 col-md-2 d-flex'>
<ScoreField score={site._source.score} maxScore={15} />
</div>
</div>
</Link>
);
rows.push(row);
rows.push(<SearchResultItem key={site._source.url} site={site} />);
});
}
@ -149,7 +143,14 @@ class SitesSearch extends Component {
</div>
);
var noresult = [placeholder, improve];
var favorites = null;
if (this.state.favorites.length > 0) {
favorites = (
<FavoritesList key='favs' urls={this.state.favorites} sizeLimit={this.itemsPerPage}/>
);
}
var noresult = [favorites, placeholder, improve];
var resultFound = (
<div className='row results'>
<div className='col-12'>
@ -182,4 +183,8 @@ class SitesSearch extends Component {
}
}
SitesSearch.propTypes = {
sitesCount: PropTypes.number,
};
export default SitesSearch;

47
src/SitesSearch.test.js

@ -3,53 +3,8 @@ import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from "react-router-dom";
import SitesSearch from './SitesSearch';
const results = [
{
"created": "2018-10-02T14:02:43.285807+00:00",
"input_url": "http://die-gruenen-bissendorf.de/",
"meta": {
"city": "Bissendorf",
"district": "Osnabrück-Land",
"level": "DE:ORTSVERBAND",
"state": "Niedersachsen",
"type": "REGIONAL_CHAPTER"
},
"score": 6.5
},
{
"created": "2018-10-02T14:38:33.424517+00:00",
"input_url": "http://die-gruenen-burscheid.de/",
"meta": {
"city": "Burscheid",
"district": "Rheinisch-Bergischer Kreis",
"level": "DE:ORTSVERBAND",
"state": "Nordrhein-Westfalen",
"type": "REGIONAL_CHAPTER"
},
"score": 8.5
},
{
"created": "2018-10-02T11:25:27.604681+00:00",
"input_url": "http://die-gruenen-meppen.de/",
"meta": {
"city": "Meppen",
"district": "Emsland-Süd",
"level": "DE:ORTSVERBAND",
"state": "Niedersachsen",
"type": "REGIONAL_CHAPTER"
},
"score": 6.5
},
];
const screenshots = {
"http://www.die-gruenen-bissendorf.de/": "93695b13199eb7b301b967aae03b8fde.png",
"http://die-gruenen-burscheid.de/": "0ac84f36d27c5d5b8f10657fa5a501bb.png",
"http://die-gruenen-meppen.de/": "9eb95b52e37211ca0c2e1c9fb54be2ec.png",
}
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Router><SitesSearch results={results} screenshots={screenshots}/></Router>, div);
ReactDOM.render(<Router><SitesSearch sitesCount={1000} /></Router>, div);
ReactDOM.unmountComponentAtNode(div);
});

11
src/index.css

@ -33,4 +33,13 @@ h5 a {
abbr[title], abbr[data-original-title] {
text-decoration: none;
}
}
button.btn-primary {
background-color: #46962b;
border-color: #46962b;
}
button.btn-primary:hover {
background-color: #61aa48;
border-color: #61aa48;
}

62
src/lib/Favorites.js

@ -0,0 +1,62 @@
export class Favorites {
store = window.localStorage;
itemName = 'favourites_v1'
/**
* Reads all favourites
*/
getAll() {
var favsString = this.store.getItem(this.itemName);
if (favsString === null || typeof(favsString) === 'undefined' || favsString === '') {
return [];
}
return favsString.split(' ');
}
/**
* Returns true if favorites include the given key
*
* @param String key
*/
include(key) {
var favs = this.getAll();
if (favs.includes(key)) {
return true;
}
return false;
}
/**
* Add an item to the favorites
* @param String key
*/
add(key) {
var favs = this.getAll();
if (favs.includes(key)) {
return;
}
favs.push(key);
favs.sort();
var favsString = favs.join(' ');
this.store.setItem(this.itemName, favsString);
}
/**
* Remove a key from the favorites
*
* @param String key
*/
remove(key) {
var favs = this.getAll();
var filtered = favs.filter(function(value, index, arr){
return value !== key;
});
var favsString = filtered.join(' ');
this.store.setItem(this.itemName, favsString);
}
}

2
yarn.lock

@ -6709,7 +6709,7 @@ prompts@^0.1.9:
kleur "^2.0.1"
sisteransi "^0.1.1"
prop-types@^15.5.8:
prop-types@^15.5.8, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==

Loading…
Cancel
Save