docbrown/app/javascript/readingList/readingList.jsx
Nick Taylor 86deddbb75
Fixed Reading List on Mobile + Small Improvements (#12755)
* Some div soup to semantic markup.

* Small refactor to inline mapping of available tags.

* Renamed <ItemListTags /> component to <TagList />.

* Now a select is used for picking a tag to filter on.

* Added custom Cypress command to create an article.

* Added documentation for the create article custom command.

* Removed unnecesary properties from payload to create an article.

* reading list mobile view wip.

* Reworked styles in <TagList />.

* Reworked reading list to use <MediaQuery /> component.

* Removed bottom padding from reading list header.

* styling tweaks if there are no available tags.

* Added some E2E tests.

* Removed reading list component test in favour of e2e test.

* Made breakpoint values numbers.

* Added some padding and more grid gap to filter on small screens.

* Adjusted jest coverage thresholds as we're moving some tests to e2e tests.

* Reverting a VS Code setting change caused by one of my extensions.

* First pass for E2E tests for the reading list.

* Added some more grid gap.

* Fixed load next page to send tags properly.

* Added some more tests.

* Improved label and placeholder for text filter in reading list.

* Added more tests

* Fixed media queries so it works in Chrome as well.

* Removed aside as tag filters are not complimentary information.

* Update app/javascript/readingList/components/TagList.jsx

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>

* Update docs/tests/e2e-tests.md

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>

* Turned off deprecated rule in jsx-a11y eslint plugin.

* Reverted to links instead of radio buttons.

* Added an all tags link and select option.

* Fixed relayout issue.

* Fixed View Archive button size.

* Fixed styling of the load more button.

* Fixed empty list issue toggling between archive and reading list.

* Fixed request changes from PR review.

* Removed CSS change that is no longer required.

* Trigger Build

* Fixed centering of items in top fieldset.

* Fixed issue with search text field resetting reading list.

* Fixed component tests for the reading list.

* Fixed empty state popping up between search queries.

* Fixed casing of fixture filenames.

* Update app/javascript/readingList/readingList.jsx

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>

* Reverted change in reading list component test.

* Added missing JSDoc comment.

* Now links are in an unordered list.

* Promoted some CSS classes from the <nav /> to the <ul /> for spacing.

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
2021-04-08 12:53:43 +01:00

291 lines
8.6 KiB
JavaScript

import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import {
loadNextPage,
onSearchBoxType,
performInitialSearch,
search,
selectTag,
clearSelectedTags,
} from '../searchableItemList/searchableItemList';
import { ItemListItem } from './components/ItemListItem';
import { ItemListItemArchiveButton } from './components/ItemListItemArchiveButton';
import { TagList } from './components/TagList';
import { MediaQuery } from '@components/MediaQuery';
import { BREAKPOINTS } from '@components/useMediaQuery';
import { debounceAction } from '@utilities/debounceAction';
import { Button } from '@crayons';
import { request } from '@utilities/http';
const NO_RESULTS_WITH_FILTER_MESSAGE = 'Nothing with this filter 🤔';
const STATUS_VIEW_VALID = 'valid,confirmed';
const STATUS_VIEW_ARCHIVED = 'archived';
const READING_LIST_ARCHIVE_PATH = '/readinglist/archive';
const READING_LIST_PATH = '/readinglist';
function ItemList({ items, archiveButtonLabel, toggleArchiveStatus }) {
return items.map((item) => {
return (
<ItemListItem item={item}>
<ItemListItemArchiveButton
text={archiveButtonLabel}
onClick={(e) => toggleArchiveStatus(e, item)}
/>
</ItemListItem>
);
});
}
export class ReadingList extends Component {
constructor(props) {
super(props);
const { statusView } = this.props;
this.state = {
archiving: false,
query: '',
index: null,
page: 0,
hitsPerPage: 80,
items: [],
itemsLoaded: false,
itemsTotal: 0,
availableTags: [],
selectedTag: '',
showLoadMoreButton: false,
statusView,
};
// bind and initialize all shared functions
this.onSearchBoxType = debounceAction(onSearchBoxType.bind(this), {
leading: true,
});
this.loadNextPage = loadNextPage.bind(this);
this.performInitialSearch = performInitialSearch.bind(this);
this.search = search.bind(this);
this.selectTag = selectTag.bind(this);
this.clearSelectedTags = clearSelectedTags.bind(this);
}
componentDidMount() {
const { statusView } = this.state;
this.performInitialSearch({
searchOptions: { status: `${statusView}` },
});
}
toggleStatusView = (event) => {
event.preventDefault();
const { query, selectedTag } = 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: [], selectedTag });
this.search(query, {
page: 0,
tags: selectedTag ? [selectedTag] : [],
statusView: newStatusView,
});
// change path in the address bar
window.history.replaceState(null, null, newPath);
};
toggleArchiveStatus = (event, item) => {
event.preventDefault();
const { statusView, items } = this.state;
request(`/reading_list_items/${item.id}`, {
method: 'PUT',
body: { current_status: statusView },
});
const newItems = items;
newItems.splice(newItems.indexOf(item), 1);
this.setState({
archiving: true,
items: newItems,
});
// hide the snackbar in a few moments
setTimeout(() => {
this.setState({ archiving: false });
}, 1000);
};
statusViewValid() {
const { statusView } = this.state;
return statusView === STATUS_VIEW_VALID;
}
renderEmptyItems() {
const { itemsLoaded, selectedTag = '', query } = this.state;
const showMessage = selectedTag.length === 0 && query.length === 0;
if (itemsLoaded && this.statusViewValid()) {
return (
<section className="align-center p-9 py-10 color-base-80">
<h2 className="fw-bold fs-l">
{showMessage
? 'Your reading list is empty'
: NO_RESULTS_WITH_FILTER_MESSAGE}
</h2>
<p class="color-base-60 pt-2">
Click the{' '}
<span class="fw-bold">
bookmark reaction
<svg
width="24"
height="24"
viewBox="0 0 24 24"
className="crayons-icon mx-1"
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<path d="M5 2h14a1 1 0 011 1v19.143a.5.5 0 01-.766.424L12 18.03l-7.234 4.536A.5.5 0 014 22.143V3a1 1 0 011-1zm13 2H6v15.432l6-3.761 6 3.761V4z" />
</svg>
</span>
when viewing a post to add it to your reading list.
</p>
</section>
);
}
return (
<h2 className="align-center p-9 py-10 color-base-80 fw-bold fs-l">
{showMessage
? 'Your Archive is empty...'
: NO_RESULTS_WITH_FILTER_MESSAGE}
</h2>
);
}
render() {
const {
items = [],
itemsTotal,
availableTags,
selectedTag = '',
showLoadMoreButton,
archiving,
loading = false,
} = this.state;
const isStatusViewValid = this.statusViewValid();
const archiveButtonLabel = isStatusViewValid ? 'Archive' : 'Unarchive';
const snackBar = archiving ? (
<div className="snackbar">
{isStatusViewValid ? 'Archiving...' : 'Unarchiving...'}
</div>
) : (
''
);
return (
<main id="main-content">
<header className="crayons-layout l:grid-cols-2 pb-0">
<h1 class="crayons-title">
{isStatusViewValid ? 'Reading list' : 'Archive'}
{` (${itemsTotal})`}
</h1>
<fieldset className="grid gap-2 m:flex m:justify-end m:items-center l:mb-0 mb-2 px-2 m:px-0">
<legend className="hidden">Filter</legend>
<Button
onClick={(e) => this.toggleStatusView(e)}
className="whitespace-nowrap l:mr-2"
variant="outlined"
url={READING_LIST_ARCHIVE_PATH}
tagName="a"
data-no-instant
>
{isStatusViewValid ? 'View archive' : 'View reading list'}
</Button>
<input
aria-label="Filter reading list by text"
onKeyUp={this.onSearchBoxType}
placeholder="Enter some text to filter on..."
className="crayons-textfield"
/>
<MediaQuery
query={`(max-width: ${BREAKPOINTS.Medium - 1}px)`}
render={(matches) => {
return (
matches && (
<TagList
availableTags={availableTags}
selectedTag={selectedTag}
onSelectTag={this.selectTag}
isMobile={true}
/>
)
);
}}
/>
</fieldset>
</header>
<MediaQuery
query={`(min-width: ${BREAKPOINTS.Medium}px)`}
render={(matches) => {
return (
<div className="crayons-layout crayons-layout--2-cols">
{matches && (
<div className="crayons-layout__sidebar-left">
<TagList
availableTags={availableTags}
selectedTag={selectedTag}
onSelectTag={this.selectTag}
/>
</div>
)}
<section className="crayons-layout__content crayons-card mb-4">
{items.length > 0 ? (
<ItemList
items={items}
archiveButtonLabel={archiveButtonLabel}
toggleArchiveStatus={this.toggleArchiveStatus}
/>
) : loading ? null : (
this.renderEmptyItems()
)}
{showLoadMoreButton && (
<div className="flex justify-center my-2">
<Button
onClick={this.loadNextPage}
variant="secondary"
className="w-max"
>
Load more
</Button>
</div>
)}
</section>
</div>
);
}}
/>
{snackBar}
</main>
);
}
}
ReadingList.defaultProps = {
statusView: STATUS_VIEW_VALID,
};
ReadingList.propTypes = {
availableTags: PropTypes.arrayOf(PropTypes.string).isRequired,
statusView: PropTypes.oneOf([STATUS_VIEW_VALID, STATUS_VIEW_ARCHIVED]),
};