docbrown/app/javascript/listings/listings.jsx
ludwiczakpawel 313cd61ee3
Listings redesign (#9349)
* flare tag line height

* .

* dropdown fix + actions bar fix

* actions bar on mob

* .

* css commented out

* .

* button fix

* preload adjustments

* tags

* Fixed listings modal to make it semantic HTML via <dialog />.

* Refactor an a11y fix.

* Removed test that is no longer needed.

* Added missing global function for test..

* Fixed broken <ListingFiltersCategories /> tests.

* Refactor of mobile dropdown for listing categories.

* Updated a TODO.

* Made the clear query button a crayons Preact button.

* padding

* Fixed listings modal for accessiblilty.

* Removed rspecs that are no longer valid.

* We're no longer using the <dialog /> element, at least for now, so CSS changes aren't necessary.

* Wasn't supposed to be committed.

Co-authored-by: Nick Taylor <nick@dev.to>
Co-authored-by: rhymes <rhymes@hey.com>
Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
2020-07-30 17:53:35 -04:00

334 lines
9.3 KiB
JavaScript

import { h, Component } from 'preact';
import debounceAction from '../utilities/debounceAction';
import { fetchSearch } from '../utilities/search';
import ModalBackground from './components/ModalBackground';
import Modal from './components/Modal';
import AllListings from './components/AllListings';
import ListingFilters from './components/ListingFilters';
import {
LISTING_PAGE_SIZE,
MATCH_LISTING,
updateListings,
getQueryParams,
resizeAllMasonryItems,
getLocation,
} from './utils';
export class Listings extends Component {
state = {
listings: [],
query: '',
tags: [],
category: '',
allCategories: [],
initialFetch: true,
currentUserId: null,
openedListing: null,
message: '',
slug: null,
page: 0,
showNextPageButton: false,
};
componentWillMount() {
const params = getQueryParams();
const container = document.getElementById('listings-index-container');
const category = container.dataset.category || '';
const allCategories = JSON.parse(container.dataset.allcategories || []);
let tags = [];
let openedListing = null;
let slug = null;
let listings = [];
if (params.t) {
tags = params.t.split(',');
}
const query = params.q || '';
if (tags.length === 0 && query === '') {
listings = JSON.parse(container.dataset.listings);
}
if (container.dataset.displayedlisting) {
openedListing = JSON.parse(container.dataset.displayedlisting);
({ slug } = openedListing);
document.body.classList.add('modal-open');
}
this.debouncedListingSearch = debounceAction(this.handleQuery.bind(this), {
time: 150,
config: { leading: true },
});
this.setState({
query,
tags,
category,
allCategories,
listings,
openedListing,
slug,
});
this.listingSearch(query, tags, category, slug);
this.setUser();
document.body.addEventListener('keydown', this.handleKeyDown);
/*
The width of the columns also changes when the browser is resized
so we will also call this function on window resize to recalculate
each grid item's height to avoid content overflow
*/
window.addEventListener('resize', resizeAllMasonryItems);
}
componentDidUpdate() {
this.triggerMasonry();
}
componentWillUnmount() {
document.body.removeEventListener('keydown', this.handleKeyDown);
}
addTag = (e, tag) => {
e.preventDefault();
if (document.body.classList.contains('modal-open')) {
this.handleCloseModal('close-modal');
}
const { query, tags, category } = this.state;
const newTags = tags;
if (newTags.indexOf(tag) === -1) {
newTags.push(tag);
}
this.setState({ tags: newTags, page: 0 });
this.listingSearch(query, newTags, category, null);
window.scroll(0, 0);
};
removeTag = (e, tag) => {
e.preventDefault();
const { query, tags, category } = this.state;
const newTags = tags;
const index = newTags.indexOf(tag);
if (newTags.indexOf(tag) > -1) {
newTags.splice(index, 1);
}
this.setState({ tags: newTags, page: 0 });
this.listingSearch(query, newTags, category, null);
};
handleKeyPressedOnSelectedTags = (e, tag) => {
if (e.key === 'Enter') {
this.removeTag(e, tag);
}
};
selectCategory = (e, cat = '') => {
e.preventDefault();
const { query, tags } = this.state;
this.setState({ category: cat, page: 0 });
this.listingSearch(query, tags, cat, null);
};
handleKeyDown = (e) => {
// Enable Escape key to close an open listing.
this.handleCloseModal(e);
};
handleCloseModal = (e) => {
const { openedListing } = this.state;
if (
e === 'close-modal' ||
(openedListing !== null && e.key === 'Escape') ||
MATCH_LISTING.includes(e.target.id)
) {
const { query, tags, category } = this.state;
this.setState({ openedListing: null, page: 0 });
this.setLocation(query, tags, category, null);
document.body.classList.remove('modal-open');
}
};
handleOpenModal = (e, listing) => {
e.preventDefault();
this.setState({ openedListing: listing });
window.history.replaceState(
null,
null,
`/listings/${listing.category}/${listing.slug}`,
);
this.setLocation(null, null, listing.category, listing.slug);
document.body.classList.add('modal-open');
};
handleDraftingMessage = (e) => {
e.preventDefault();
this.setState({ message: e.target.value });
};
handleSubmitMessage = (e) => {
e.preventDefault();
const { message, openedListing } = this.state;
if (message.replace(/\s/g, '').length === 0) {
return;
}
const formData = new FormData();
formData.append('user_id', openedListing.user_id);
formData.append('message', `**re: ${openedListing.title}** ${message}`);
formData.append('controller', 'chat_channels');
const destination = `/connect/@${openedListing.author.username}`;
const metaTag = document.querySelector("meta[name='csrf-token']");
window
.fetch('/chat_channels/create_chat', {
method: 'POST',
headers: {
'X-CSRF-Token': metaTag.getAttribute('content'),
},
body: formData,
credentials: 'same-origin',
})
.then(() => {
window.location.href = destination;
});
};
handleQuery = (e) => {
const { tags, category } = this.state;
this.setState({ query: e.target.value, page: 0 });
this.listingSearch(e.target.value, tags, category, null);
};
clearQuery = () => {
const { tags, category } = this.state;
document.getElementById('listings-search').value = '';
this.setState({ query: '', page: 0 });
this.listingSearch('', tags, category, null);
};
loadNextPage = () => {
const { query, tags, category, slug, page } = this.state;
this.setState({ page: page + 1 });
this.listingSearch(query, tags, category, slug);
};
setUser = () => {
const { currentUserId } = this.state;
setTimeout(() => {
if (window.currentUser && currentUserId === null) {
this.setState({ currentUserId: window.currentUser.id });
}
}, 1000);
};
triggerMasonry = () => {
resizeAllMasonryItems();
setTimeout(resizeAllMasonryItems, 1);
setTimeout(resizeAllMasonryItems, 3);
};
setLocation = (query, tags, category, slug) => {
const newLocation = getLocation({ query, tags, category, slug });
window.history.replaceState(null, null, newLocation);
};
/**
* Call search API for Listings
*
* @param {string} query - The search term
* @param {string} tags - The tags selected by the user
* @param {string} category - The category selected by the user
* @param {string} slug - The listing's slug
*
* @returns {Promise} A promise object with response formatted as JSON.
*/
listingSearch(query, tags, category, slug) {
const { page } = this.state;
const dataHash = {
category,
listing_search: query,
page,
per_page: LISTING_PAGE_SIZE,
tags,
tag_boolean_mode: 'all',
};
const responsePromise = fetchSearch('listings', dataHash);
return responsePromise.then((response) => {
const listings = response.result;
const fullListings = updateListings(listings);
this.setState({
listings: fullListings,
initialFetch: false,
showNextPageButton: listings.length === LISTING_PAGE_SIZE,
});
this.setLocation(query, tags, category, slug);
});
}
render() {
const {
listings,
query,
tags,
category,
allCategories,
currentUserId,
openedListing,
showNextPageButton,
initialFetch,
message,
} = this.state;
const shouldRenderModal =
openedListing !== null && openedListing !== undefined;
if (initialFetch) {
this.triggerMasonry();
}
return (
<div className="crayons-layout crayons-layout--2-cols">
<ListingFilters
categories={allCategories}
category={category}
onSelectCategory={this.selectCategory}
message={message}
onKeyUp={this.debouncedListingSearch}
onClearQuery={this.clearQuery}
onRemoveTag={this.removeTag}
tags={tags}
onKeyPress={this.handleKeyPressedOnSelectedTags}
query={query}
/>
<AllListings
listings={listings}
onAddTag={this.addTag}
onChangeCategory={this.selectCategory}
currentUserId={currentUserId}
onOpenModal={this.handleOpenModal}
showNextPageButton={showNextPageButton}
loadNextPage={this.loadNextPage}
/>
{shouldRenderModal && (
<div className="crayons-modal">
<Modal
currentUserId={currentUserId}
onAddTag={this.addTag}
onChangeDraftingMessage={this.handleDraftingMessage}
onClick={this.handleCloseModal}
onChangeCategory={this.selectCategory}
onOpenModal={this.handleOpenModal}
onSubmit={this.handleSubmitMessage}
listing={openedListing}
message={message}
/>
<ModalBackground onClick={this.handleCloseModal} />
</div>
)}
</div>
);
}
}
Listings.displayName = 'Classified Listings';