docbrown/app/javascript/listings/listings.jsx
Michael Kohl 7f75f99560
[deploy] Rename classified listings (#7910)
* Change models and related files

* Update controllers and specs

* More renaming

* Seek and destroy, I mean search and replace

* Round up the stragglers

* Ground control to Major Travis...

* More fixes

* PR feedback

* Various fixes

* Rename view

* Fix list query builder

* Unify request specs

* Fix some API spec errors

* Fix remaining API specs

* Make spec conform to API

* Fix leftover problems

* Fix JS tests

* Fix column name in select

* Fix API specs

* Fix search specs

* Paging Mr. Travis
2020-05-27 13:35:09 +00:00

334 lines
9.2 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 NextPageButton from './components/NextPageButton';
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="listings__container">
{shouldRenderModal && (
<ModalBackground onClick={this.handleCloseModal} />
)}
<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 && <NextPageButton onClick={this.loadNextPage} />}
{shouldRenderModal && (
<Modal
currentUserId={currentUserId}
onAddTag={this.addTag}
onChange={this.handleDraftingMessage}
onClick={this.handleCloseModal}
onChangeCategory={this.selectCategory}
onOpenModal={this.handleOpenModal}
onSubmit={this.handleSubmitMessage}
listing={openedListing}
message={message}
/>
)}
</div>
);
}
}
Listings.displayName = 'Classified Listings';