docbrown/app/javascript/readingList/readingList.jsx

274 lines
7.4 KiB
JavaScript

import { h, Component } from 'preact';
import { PropTypes } from 'preact-compat';
import debounce from 'lodash.debounce';
import {
defaultState,
loadNextPage,
onSearchBoxType,
performInitialSearch,
search,
toggleTag,
clearSelectedTags,
} from '../searchableItemList/searchableItemList';
import { ItemListItem } from '../src/components/ItemList/ItemListItem';
import { ItemListItemArchiveButton } from '../src/components/ItemList/ItemListItemArchiveButton';
import { ItemListLoadMoreButton } from '../src/components/ItemList/ItemListLoadMoreButton';
import { ItemListTags } from '../src/components/ItemList/ItemListTags';
const STATUS_VIEW_VALID = 'valid';
const STATUS_VIEW_ARCHIVED = 'archived';
const READING_LIST_ARCHIVE_PATH = '/readinglist/archive';
const READING_LIST_PATH = '/readinglist';
const FilterText = ({ selectedTags, query, value }) => {
return (
<h1>
{selectedTags.length === 0 && query.length === 0
? value
: 'Nothing with this filter 🤔'}
</h1>
);
};
export class ReadingList extends Component {
constructor(props) {
super(props);
const { availableTags, statusView } = this.props;
this.state = defaultState({ availableTags, archiving: false, statusView });
// bind and initialize all shared functions
this.onSearchBoxType = debounce(onSearchBoxType.bind(this), 300, {
leading: true,
});
this.loadNextPage = loadNextPage.bind(this);
this.performInitialSearch = performInitialSearch.bind(this);
this.search = search.bind(this);
this.toggleTag = toggleTag.bind(this);
this.clearSelectedTags = clearSelectedTags.bind(this);
}
componentDidMount() {
const { hitsPerPage, statusView } = this.state;
this.performInitialSearch({
containerId: 'reading-list',
indexName: 'SecuredReactions',
searchOptions: {
hitsPerPage,
filters: `status:${statusView}`,
},
});
}
toggleStatusView = event => {
event.preventDefault();
const { query, selectedTags } = this.state;
const isStatusViewValid = this.statusViewValid();
const newStatusView = isStatusViewValid
? STATUS_VIEW_ARCHIVED
: STATUS_VIEW_VALID;
const newPath = isStatusViewValid
? READING_LIST_ARCHIVE_PATH
: READING_LIST_PATH;
// empty items so that changing the view will start from scratch
this.setState({ statusView: newStatusView, items: [] });
this.search(query, {
page: 0,
tags: selectedTags,
statusView: newStatusView,
});
// change path in the address bar
window.history.replaceState(null, null, newPath);
};
toggleArchiveStatus = (event, item) => {
event.preventDefault();
const { statusView, items, totalCount } = this.state;
window.fetch(`/reading_list_items/${item.id}`, {
method: 'PUT',
headers: {
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ current_status: statusView }),
credentials: 'same-origin',
});
const t = this;
const newItems = items;
newItems.splice(newItems.indexOf(item), 1);
t.setState({
archiving: true,
items: newItems,
totalCount: totalCount - 1,
});
// hide the snackbar in a few moments
setTimeout(() => {
t.setState({ archiving: false });
}, 1000);
};
statusViewValid() {
const { statusView } = this.state;
return statusView === STATUS_VIEW_VALID;
}
renderEmptyItems() {
const { itemsLoaded, selectedTags, query } = this.state;
if (itemsLoaded && this.statusViewValid()) {
return (
<div className="items-empty">
<FilterText
selectedTags={selectedTags}
query={query}
value="Your Reading List is Lonely"
/>
<h3>
Hit the
<span className="highlight">SAVE</span>
or
<span className="highlight">
Bookmark
<span role="img" aria-label="Bookmark">
🔖
</span>
</span>
to start your Collection
</h3>
</div>
);
}
return (
<div className="items-empty">
<FilterText
selectedTags={selectedTags}
query={query}
value="Your Archive List is Lonely"
/>
</div>
);
}
render() {
const {
items,
itemsLoaded,
totalCount,
availableTags,
selectedTags,
showLoadMoreButton,
archiving,
} = this.state;
const isStatusViewValid = this.statusViewValid();
const archiveButtonLabel = isStatusViewValid ? 'archive' : 'unarchive';
const itemsToRender = items.map(item => {
return (
<ItemListItem item={item}>
<ItemListItemArchiveButton
text={archiveButtonLabel}
onClick={e => this.toggleArchiveStatus(e, item)}
/>
</ItemListItem>
);
});
const snackBar = archiving ? (
<div className="snackbar">
{isStatusViewValid ? 'Archiving...' : 'Unarchiving...'}
</div>
) : (
''
);
return (
<div className="home item-list">
<div className="side-bar">
<div className="widget filters">
<input
onKeyUp={this.onSearchBoxType}
placeHolder="search your list"
/>
<div className="filters-header">
<h4 className="filters-header-text">my tags</h4>
{Boolean(selectedTags.length) && (
<a
className="filters-header-action"
href={
isStatusViewValid
? READING_LIST_PATH
: READING_LIST_ARCHIVE_PATH
}
onClick={this.clearSelectedTags}
data-no-instant
>
clear all
</a>
)}
</div>
<ItemListTags
availableTags={availableTags}
selectedTags={selectedTags}
onClick={this.toggleTag}
/>
<div className="status-view-toggle">
<a
href={READING_LIST_ARCHIVE_PATH}
onClick={e => this.toggleStatusView(e)}
data-no-instant
>
{isStatusViewValid ? 'View Archive' : 'View Reading List'}
</a>
</div>
</div>
</div>
<div className="items-container">
<div className={`results ${itemsLoaded ? 'results--loaded' : ''}`}>
<div className="results-header">
{isStatusViewValid ? 'Reading List' : 'Archive'}
{` (${totalCount > 0 ? totalCount : 'empty'})`}
</div>
<div>
{items.length > 0 ? itemsToRender : this.renderEmptyItems()}
</div>
</div>
<ItemListLoadMoreButton
show={showLoadMoreButton}
onClick={this.loadNextPage}
/>
</div>
{snackBar}
</div>
);
}
}
ReadingList.defaultProps = {
statusView: STATUS_VIEW_VALID,
};
ReadingList.propTypes = {
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired,
statusView: PropTypes.oneOf([STATUS_VIEW_VALID, STATUS_VIEW_ARCHIVED]),
};
FilterText.propTypes = {
selectedTags: PropTypes.arrayOf(PropTypes.string).isRequired,
value: PropTypes.string.isRequired,
query: PropTypes.arrayOf(PropTypes.string).isRequired,
};