333 lines
9.3 KiB
JavaScript
333 lines
9.3 KiB
JavaScript
import { h, Component } from 'preact';
|
|
import debounceAction from '../src/utils/debounceAction';
|
|
import { fetchSearch } from '../src/utils/search';
|
|
import ModalBackground from './components/ModalBackground';
|
|
import Modal from './components/Modal';
|
|
import AllListings from './components/AllListings';
|
|
import NextPageButton from './components/NextPageButton';
|
|
import ClassifiedFilters from './components/ClassifiedFilters';
|
|
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('classifieds-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.debouncedClassifiedListingSearch = 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 ClassifiedListings
|
|
*
|
|
* @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,
|
|
classified_listing_search: query,
|
|
page,
|
|
per_page: LISTING_PAGE_SIZE,
|
|
tags,
|
|
tag_boolean_mode: 'all',
|
|
};
|
|
|
|
const responsePromise = fetchSearch('classified_listings', dataHash);
|
|
return responsePromise.then((response) => {
|
|
const classifiedListings = response.result;
|
|
const fullListings = updateListings(classifiedListings);
|
|
this.setState({
|
|
listings: fullListings,
|
|
initialFetch: false,
|
|
showNextPageButton: classifiedListings.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 && undefined;
|
|
|
|
if (initialFetch) {
|
|
this.triggerMasonry();
|
|
}
|
|
return (
|
|
<div className="listings__container">
|
|
{shouldRenderModal && (
|
|
<ModalBackground onClick={this.handleCloseModal} />
|
|
)}
|
|
<ClassifiedFilters
|
|
categories={allCategories}
|
|
category={category}
|
|
onSelectCategory={this.selectCategory}
|
|
message={message}
|
|
onKeyUp={this.debouncedClassifiedListingSearch}
|
|
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';
|