* adds focusTrap wrapper to preact Modal component * add view specific code for focus trap in admin add nav link * add script to return a focustrap toggle, use in add nav link modal partial * add trap to edit nav link modal * add handlers for sign up modal * update modal controller for admin section, update nav link modals to use * update other admin modals with new data values for trap * remove unneeded erb script file * remove unneeded target * refactor to remove extra unneeded param * remove duplicate code, store getFocusTrapToggle in window * trap focus in comment and bookmark showModal instances for not logged in user * remove need for activator id * clean up id refs no longer needed * remove custom code and re-use focsu-trap lib * update storybook docs * update default export in focusTrap * prevent close button click triggering a modal toggle twice * ensure if user navigates from a modal the trap is deactivated * add jsdoc comments and add dynamic import * ensure admin controller modal traps are cleaned up on disconnect * update sign up modal to use crayons * update modal controller and admin nav links modals to use preact modal * update profile fields modals for new controller * tweak styling of sign up and admin modals to match previous * update listings modal to use crayons modal, adapt focus trap to work with click outside * memoize deactivate callback to ensure modal can be presented on first page load * add missed focustrap changes * fix focus trap issues in onboarding flow * refactor onboarding focus trap, remove getFocusTrapToggle * tweaks for styling and article modal toggle * add click outside tests to modal * add cypress tests for the login modal * update liquid tag tests affected by change * refactors to address review comments * fix issue with login modal presented twice on comment add * change ids to selectors in admin modals * small pr comment refactors * add listings e2e tests * add nav link modal tests * fix issue with help modal * tweak to fix ui bug from merge * remove context from showLoginModal * rename toggleModal * rename state property for clarity Co-authored-by: Nick Taylor <nick@dev.to>
310 lines
8.5 KiB
JavaScript
310 lines
8.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,
|
|
message: '',
|
|
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({ 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);
|
|
};
|
|
|
|
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.user.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,
|
|
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}
|
|
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 && (
|
|
<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}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
Listings.displayName = 'Classified Listings';
|