docbrown/app/javascript/listings/listings.jsx
Michael Kohl 09828853f6
✂✂✂ Remove Connect (#14734)
* Remove Connect

* Remove more Connect specs

* Remove a lot more Connect code

* 🚮

* It all has to go

* Explicitly add httpclient

* Update application layout

* Remove messages association from User

* Start fixing specs

* reintroduce util function and refactor references

* Remove Connect Cypress test

* Fix more specs

* Remove Connect from listings

* Ignore contact_via_connect column on listings

* Remove contact_via_connect usages

* Ignore mod_chat_channel_id on tags

* Drop Connect tables

* Remove email_connect_messages from user notification settings

* Re-add httpclient 2.8.3

This was mistakenly removed as a merge conflict

* Don't need to exclude removed chat channel file

* Remove unneeded style for chat channels

* Remove unneeded channel list prop type

* Remove chat channels index/connect-link from getPageEntries

* Re-add comment from httpclient in Gemfile

* Remove connect references from mailers

Tag Moderators no longer have a chat channel

No longer will users be notified about new messages (there won't be
any)

No longer will users be notified about channel invites (you can't
invite anyone anymore)

* Don't configure Pusher and remove PUSHER_* from .env_sample

since it's removed from gemfile, the Pusher constant will not resolve, if this is
configured in the environment variables we'll fail to boot.

Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
Co-authored-by: Dan Uber <dan@forem.com>
2021-11-18 08:21:00 -06:00

277 lines
7.5 KiB
JavaScript

import { h, Component } from 'preact';
import { debounceAction } from '../utilities/debounceAction';
import { fetchSearch } from '../utilities/search';
import { Modal } from './components/Modal';
import { AllListings } from './components/AllListings';
import { ListingFilters } from './components/ListingFilters';
import {
LISTING_PAGE_SIZE,
updateListings,
getQueryParams,
resizeAllMasonryItems,
getLocation,
} from './utils';
export class Listings extends Component {
state = {
listings: [],
query: '',
tags: [],
category: '',
allCategories: [],
initialFetch: true,
currentUserId: null,
openedListing: null,
slug: null,
page: 0,
showNextPageButton: false,
isModalOpen: 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 = [];
let isModalOpen = false;
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);
isModalOpen = true;
}
this.debouncedListingSearch = debounceAction(this.handleQuery.bind(this), {
time: 150,
config: { leading: true },
});
this.setState({
query,
tags,
category,
allCategories,
listings,
openedListing,
slug,
isModalOpen,
});
this.listingSearch(query, tags, category, slug);
this.setUser();
/*
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();
}
addTag = (e, tag) => {
e.preventDefault();
const { query, tags, category } = this.state;
const newTags = tags;
if (newTags.indexOf(tag) === -1) {
newTags.push(tag);
}
this.setState({ tags: newTags, page: 0, isModalOpen: false });
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((prevState) => {
if (prevState.isModalOpen) {
return { category: cat, page: 0, isModalOpen: false };
}
return { category: cat, page: 0 };
});
this.listingSearch(query, tags, cat, null);
};
handleCloseModal = () => {
const { query, tags, category } = this.state;
this.setState({ openedListing: null, page: 0, isModalOpen: false });
this.setLocation(query, tags, category, null);
};
handleOpenModal = (e, listing) => {
e.preventDefault();
this.setState({ openedListing: listing, isModalOpen: true });
window.history.replaceState(
null,
null,
`/listings/${listing.category}/${listing.slug}`,
);
this.setLocation(null, null, listing.category, listing.slug);
};
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,
isModalOpen,
} = this.state;
const shouldRenderModal =
isModalOpen && 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}
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 && (
<Modal
currentUserId={currentUserId}
onAddTag={this.addTag}
onClick={this.handleCloseModal}
onChangeCategory={this.selectCategory}
onOpenModal={this.handleOpenModal}
listing={openedListing}
/>
)}
</div>
);
}
}
Listings.displayName = 'Classified Listings';