* init
- add reading_time to reactions
- make migration
- add to algolia index in reaction model
- add method in reaction model to define reading time based on reactable reading time
* add time to front end
* Revert "Return errors instead of raising an exception on ArticleCreation (#2922)"
This reverts commit 09a99d93a3.
* Revert "init"
This reverts commit 53650ebcc3c93c54e396977f7c34f7aa35a589c4.
* Revert "Revert "Return errors instead of raising an exception on ArticleCreation (#2922)""
This reverts commit e8cf1f3a290dd06b937247e3ff7593e7c7036553.
* Update reaction.rb
* remove duplicate searchableAttribute
238 lines
7.1 KiB
JavaScript
238 lines
7.1 KiB
JavaScript
import { h, Component } from 'preact';
|
|
|
|
export class ReadingList extends Component {
|
|
state = {
|
|
readingListItems: [],
|
|
query: '',
|
|
index: '',
|
|
availableTags: [],
|
|
selectedTags: [],
|
|
itemsLoaded: false,
|
|
archiving: false,
|
|
statusView: document.getElementById('reading-list').dataset.view,
|
|
};
|
|
|
|
componentDidMount() {
|
|
const algoliaId = document.querySelector("meta[name='algolia-public-id']")
|
|
.content;
|
|
const algoliaKey = document.getElementById('reading-list').dataset.algolia;
|
|
const env = document.querySelector("meta[name='environment']").content;
|
|
const client = algoliasearch(algoliaId, algoliaKey);
|
|
const index = client.initIndex(`SecuredReactions_${env}`);
|
|
const t = this;
|
|
index
|
|
.search('', { hitsPerPage: 64, filters: `status:${t.state.statusView}` })
|
|
.then(content => {
|
|
t.setState({
|
|
readingListItems: content.hits,
|
|
index,
|
|
itemsLoaded: true,
|
|
});
|
|
});
|
|
const waitingOnUser = setInterval(() => {
|
|
if (window.currentUser) {
|
|
t.setState({ availableTags: window.currentUser.followed_tag_names });
|
|
clearInterval(waitingOnUser);
|
|
}
|
|
}, 1);
|
|
}
|
|
|
|
handleTyping = e => {
|
|
const query = e.target.value;
|
|
const { selectedTags, statusView } = this.state;
|
|
this.listSearch(query, selectedTags, statusView);
|
|
};
|
|
|
|
toggleTag = (e, tag) => {
|
|
e.preventDefault();
|
|
const { query, selectedTags, statusView } = this.state;
|
|
const newTags = selectedTags;
|
|
if (newTags.indexOf(tag) === -1) {
|
|
newTags.push(tag);
|
|
} else {
|
|
newTags.splice(newTags.indexOf(tag), 1);
|
|
}
|
|
this.setState({ selectedTags: newTags });
|
|
this.listSearch(query, newTags, statusView);
|
|
};
|
|
|
|
toggleStatusView = e => {
|
|
e.preventDefault();
|
|
const { statusView, query, selectedTags } = this.state;
|
|
if (statusView === 'valid') {
|
|
this.setState({ statusView: 'archived' });
|
|
this.listSearch(query, selectedTags, 'archived');
|
|
window.history.replaceState(null, null, '/readinglist/archive');
|
|
} else {
|
|
this.setState({ statusView: 'valid' });
|
|
this.listSearch(query, selectedTags, 'valid');
|
|
window.history.replaceState(null, null, '/readinglist');
|
|
}
|
|
};
|
|
|
|
archive = (e, item) => {
|
|
e.preventDefault();
|
|
const { statusView, readingListItems } = this.state;
|
|
const t = this;
|
|
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 newItems = readingListItems;
|
|
newItems.splice(newItems.indexOf(item), 1);
|
|
t.setState({ archiving: true, readingListItems: newItems });
|
|
setTimeout(() => {
|
|
t.setState({ archiving: false });
|
|
}, 1800);
|
|
};
|
|
|
|
listSearch(query, tags, statusView) {
|
|
const t = this;
|
|
const { index } = this.state;
|
|
const filters = { hitsPerPage: 256, filters: `status:${statusView}` };
|
|
if (tags.length > 0) {
|
|
filters.tagFilters = tags;
|
|
}
|
|
index.search(query, filters).then(content => {
|
|
t.setState({ readingListItems: content.hits, query });
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
readingListItems,
|
|
availableTags,
|
|
selectedTags,
|
|
itemsLoaded,
|
|
query,
|
|
statusView,
|
|
archiving,
|
|
} = this.state;
|
|
let allItems = readingListItems.map(item => (
|
|
<div className="readinglist-item-wrapper">
|
|
<a className="readinglist-item" href={item.searchable_reactable_path}>
|
|
<div className="readinglist-item-title">
|
|
{item.searchable_reactable_title}
|
|
</div>
|
|
<div className="readinglist-item-details">
|
|
<a
|
|
className="readinglist-item-user"
|
|
href={`/${item.reactable_user.username}`}
|
|
>
|
|
<img
|
|
src={item.reactable_user.profile_image_90}
|
|
alt="Profile Pic"
|
|
/>
|
|
{item.reactable_user.name}・{item.reactable_published_date}・
|
|
{item.reading_time} min read・
|
|
</a>
|
|
<span className="readinglist-item-tag-collection">
|
|
{item.reactable_tags.map(tag => (
|
|
<a className="readinglist-item-tag" href={`/t/${tag}`}>
|
|
#{tag}
|
|
</a>
|
|
))}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
<button
|
|
className="readinglist-archive-butt"
|
|
onClick={e => this.archive(e, item)}
|
|
type="button"
|
|
>
|
|
{statusView === 'valid' ? 'archive' : 'unarchive'}
|
|
</button>
|
|
</div>
|
|
));
|
|
if (readingListItems.length === 0 && itemsLoaded) {
|
|
if (statusView === 'valid') {
|
|
allItems = (
|
|
<div className="readinglist-empty">
|
|
<h1>
|
|
{selectedTags.length === 0 && query.length === 0
|
|
? 'Your Reading List is Lonely'
|
|
: 'Nothing with this filter 🤔'}
|
|
</h1>
|
|
<h3>
|
|
Hit the
|
|
<span class="highlight">SAVE</span>
|
|
or
|
|
<span class="highlight">
|
|
Bookmark
|
|
<span role="img" aria-label="Bookmark">
|
|
🔖
|
|
</span>
|
|
</span>
|
|
to start your Collection
|
|
</h3>
|
|
</div>
|
|
);
|
|
} else {
|
|
allItems = (
|
|
<div className="readinglist-empty">
|
|
<h1>
|
|
{selectedTags.length === 0 && query.length === 0
|
|
? 'Your Archive List is Lonely'
|
|
: 'Nothing with this filter 🤔'}
|
|
</h1>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
const allTags = availableTags.map(tag => (
|
|
<a
|
|
className={`readinglist-tag ${
|
|
selectedTags.indexOf(tag) > -1 ? 'selected' : ''
|
|
}`}
|
|
href={`/t/${tag}`}
|
|
data-no-instant
|
|
onClick={e => this.toggleTag(e, tag)}
|
|
>
|
|
#{tag}
|
|
</a>
|
|
));
|
|
const snackBar = archiving ? (
|
|
<div className="snackbar">
|
|
{statusView === 'valid' ? 'Archiving' : 'Unarchiving'}
|
|
(async)
|
|
</div>
|
|
) : (
|
|
''
|
|
);
|
|
return (
|
|
<div className="home readinglist-home">
|
|
<div className="side-bar">
|
|
<div className="widget readinglist-filters">
|
|
<input onKeyUp={this.handleTyping} placeHolder="search your list" />
|
|
<div className="readinglist-tags">{allTags}</div>
|
|
<div className="readinglist-view-toggle">
|
|
<a
|
|
href="/readinglist/archive"
|
|
onClick={e => this.toggleStatusView(e)}
|
|
data-no-instant
|
|
>
|
|
{statusView === 'valid' ? 'View Archive' : 'View Reading List'}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={`readinglist-results ${
|
|
itemsLoaded ? 'readinglist-results--loaded' : ''
|
|
}`}
|
|
>
|
|
<div className="readinglist-results-header">
|
|
{statusView === 'valid' ? 'Reading List' : 'Archive'}
|
|
</div>
|
|
<div>{allItems}</div>
|
|
</div>
|
|
{snackBar}
|
|
</div>
|
|
);
|
|
}
|
|
}
|