Update JS listings form and other tweaks (#2697)
This commit is contained in:
parent
04758f46f8
commit
9e7dbf8356
11 changed files with 168 additions and 113 deletions
|
|
@ -917,7 +917,7 @@
|
|||
@include themeable(background, theme-container-background, #fff);
|
||||
margin-bottom: 14px;
|
||||
border-radius: 3px;
|
||||
width: calc(100% - 4px);
|
||||
width: calc(100% - 6px);
|
||||
@include themeable(
|
||||
border,
|
||||
theme-container-border,
|
||||
|
|
@ -975,7 +975,7 @@
|
|||
margin-bottom: 20px;
|
||||
.sidebar-nav-block {
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
width: calc(100% - 4px);
|
||||
box-sizing: border-box;
|
||||
&.principle-sidebar-nav-block {
|
||||
padding: 9px 0px;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.classifieds-container {
|
||||
margin: 60px auto 150px;
|
||||
|
|
@ -204,10 +205,6 @@
|
|||
font-size: 18px;
|
||||
}
|
||||
|
||||
.listingform__input {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.listingform__bodymarkdown {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
|
@ -215,6 +212,7 @@
|
|||
font-size: 17px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
margin-top: 10px;
|
||||
background: var(--theme-container-background, white);
|
||||
color: $black;
|
||||
color: var(--theme-color, $black);
|
||||
|
|
@ -248,32 +246,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.listing__overlay {
|
||||
display: none;
|
||||
background-color: $black;
|
||||
opacity: .7;
|
||||
z-index: 10000;
|
||||
animation: fadein 0.20s ease;
|
||||
@keyframes fadein{
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: .7; }
|
||||
}
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.singlelisting__modal {
|
||||
z-index: 11000;
|
||||
position: absolute;
|
||||
top: 15%;
|
||||
left: 33%;
|
||||
max-width: 33%;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.single-classified-listing {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
|
@ -294,6 +266,7 @@
|
|||
background: var(--theme-container-accent-background, lighten($light-medium-gray, 8%));
|
||||
border-bottom: 1px solid $light-medium-gray;
|
||||
font-size: 16px;
|
||||
a { @include themeable(color, theme-color, $black); }
|
||||
}
|
||||
.single-classified-listing-body {
|
||||
padding: 10px 15px;
|
||||
|
|
@ -338,4 +311,30 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.single-classified-listing--opened {
|
||||
z-index: 20;
|
||||
position: fixed;
|
||||
top: 110px;
|
||||
width: 90%;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
padding-bottom: 12px;
|
||||
@media screen and (min-width: 950px) {
|
||||
top: calc( 50px + 15vh);
|
||||
left: 50%;
|
||||
width: 600px;
|
||||
margin-left: -300px; /* Negative half of width. */
|
||||
h3 { font-size: calc(20px + 0.05vw);}
|
||||
.single-classified-listing-body {
|
||||
font-size: calc(18px + 0.05vw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.classified-listings-modal-background {
|
||||
z-index: 15;
|
||||
position: fixed;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
user-select: none;
|
||||
}
|
||||
|
|
@ -10,13 +10,10 @@ class ClassifiedListingsController < ApplicationController
|
|||
else
|
||||
[]
|
||||
end
|
||||
@displayed_classified_listing = ClassifiedListing.find_by!(category: params[:category], slug: params[:slug]) if params[:slug]
|
||||
set_surrogate_key_header "classified-listings-#{params[:category]}"
|
||||
end
|
||||
|
||||
def show
|
||||
@classified_listing = ClassifiedListing.find_by!(category: params[:category], slug: params[:slug])
|
||||
end
|
||||
|
||||
def new
|
||||
@classified_listing = ClassifiedListing.new
|
||||
@credits = current_user.credits.where(spent: false)
|
||||
|
|
@ -96,5 +93,7 @@ class ClassifiedListingsController < ApplicationController
|
|||
def clear_listings_cache
|
||||
CacheBuster.new.bust("/listings")
|
||||
CacheBuster.new.bust("/listings?i=i")
|
||||
CacheBuster.new.bust("/listings/#{@classified_listing.category}/#{@classified_listing.slug}")
|
||||
CacheBuster.new.bust("/listings/#{@classified_listing.category}/#{@classified_listing.slug}?i=i")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,21 +5,19 @@ const domId = 1;
|
|||
|
||||
const Title = ( { onChange, defaultValue }) => (
|
||||
<div className="field">
|
||||
<label className="listingform__label" htmlFor={domId}>
|
||||
Title
|
||||
<input
|
||||
type="text"
|
||||
className="listingform__input"
|
||||
id={domId}
|
||||
name="classified_listing[title]"
|
||||
maxLength="128"
|
||||
size="128"
|
||||
placeholder="128 characters max, plain text"
|
||||
autoComplete="off"
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="listingform__label" htmlFor={domId}>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
className="listingform__input"
|
||||
id={domId}
|
||||
name="classified_listing[title]"
|
||||
maxLength="128"
|
||||
size="128"
|
||||
placeholder="128 characters max, plain text"
|
||||
autoComplete="off"
|
||||
value={defaultValue}
|
||||
onInput={onChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export class Listings extends Component {
|
|||
allCategories: [],
|
||||
initialFetch: true,
|
||||
currentUserId: null,
|
||||
openedListing: null,
|
||||
slug: null
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
|
|
@ -35,11 +37,22 @@ export class Listings extends Component {
|
|||
if (tags.length === 0 && query === '') {
|
||||
listings = JSON.parse(container.dataset.listings)
|
||||
}
|
||||
t.setState({query, tags, index, category, allCategories, listings });
|
||||
t.listingSearch(query, tags, category);
|
||||
let openedListing = null;
|
||||
let slug = null;
|
||||
if (container.dataset.displayedlisting) {
|
||||
openedListing = JSON.parse(container.dataset.displayedlisting);
|
||||
slug = openedListing.slug;
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
t.setState({query, tags, index, category, allCategories, listings, openedListing, slug });
|
||||
t.listingSearch(query, tags, category, slug);
|
||||
t.setUser()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.triggerMasonry()
|
||||
}
|
||||
|
||||
addTag = (e, tag) => {
|
||||
e.preventDefault();
|
||||
const { query, tags, category } = this.state;
|
||||
|
|
@ -48,7 +61,7 @@ export class Listings extends Component {
|
|||
newTags.push(tag)
|
||||
}
|
||||
this.setState({tags: newTags})
|
||||
this.listingSearch(query, newTags, category)
|
||||
this.listingSearch(query, newTags, category, null)
|
||||
window.scroll(0,0)
|
||||
}
|
||||
|
||||
|
|
@ -61,37 +74,42 @@ export class Listings extends Component {
|
|||
newTags.splice(index, 1);
|
||||
}
|
||||
this.setState({tags: newTags})
|
||||
this.listingSearch(query, newTags, category)
|
||||
this.listingSearch(query, newTags, category, null)
|
||||
}
|
||||
|
||||
selectCategory = (e, cat) => {
|
||||
e.preventDefault();
|
||||
const { query, tags } = this.state;
|
||||
this.setState({category: cat})
|
||||
this.listingSearch(query, tags, cat)
|
||||
this.listingSearch(query, tags, cat, null)
|
||||
}
|
||||
|
||||
handleCloseModal = (e) => {
|
||||
if (e.type === "click" || e.key === "Escape") {
|
||||
const overlay = document.getElementById('listing-overlay')
|
||||
overlay.style.display = 'none'
|
||||
overlay.tabIndex = -1
|
||||
window.history.pushState('', '', overlay.dataset.originLink)
|
||||
overlay.parentElement.removeChild(overlay.nextElementSibling)
|
||||
}
|
||||
const { query, tags, category } = this.state;
|
||||
this.setState({openedListing: null})
|
||||
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}`);
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
handleQuery = e => {
|
||||
const { tags, category } = this.state;
|
||||
this.setState({query: e.target.value})
|
||||
this.listingSearch(e.target.value, tags, category)
|
||||
this.listingSearch(e.target.value, tags, category, null)
|
||||
}
|
||||
|
||||
clearQuery = () => {
|
||||
const { tags, category } = this.state;
|
||||
document.getElementById('listings-search').value = '';
|
||||
this.setState({query: ''});
|
||||
this.listingSearch('', tags, category);
|
||||
this.listingSearch('', tags, category, null);
|
||||
}
|
||||
|
||||
getQueryParams = () => {
|
||||
|
|
@ -134,7 +152,25 @@ export class Listings extends Component {
|
|||
}, 3)
|
||||
}
|
||||
|
||||
listingSearch(query, tags, category) {
|
||||
setLocation = (query, tags, category, slug) => {
|
||||
let newLocation = ''
|
||||
if (slug) {
|
||||
|
||||
} else if (query.length > 0 && tags.length > 0) {
|
||||
newLocation = `/listings/${category}?q=${query}&t=${tags}`;
|
||||
} else if (query.length > 0){
|
||||
newLocation = `/listings/${category}?q=${query}`;
|
||||
} else if (tags.length > 0) {
|
||||
newLocation = `/listings/${category}?t=${tags}`;
|
||||
} else if (category.length > 0) {
|
||||
newLocation = `/listings/${category}`;
|
||||
} else {
|
||||
newLocation = '/listings'
|
||||
}
|
||||
window.history.replaceState(null, null, newLocation);
|
||||
}
|
||||
|
||||
listingSearch(query, tags, category, slug) {
|
||||
const t = this;
|
||||
const filterObject = {tagFilters: tags, hitsPerPage: 120,}
|
||||
if (category.length > 0) {
|
||||
|
|
@ -155,32 +191,21 @@ export class Listings extends Component {
|
|||
}
|
||||
|
||||
});
|
||||
let newLocation = ''
|
||||
if (query.length > 0 && tags.length > 0) {
|
||||
newLocation = `/listings/${category}?q=${query}&t=${tags}`;
|
||||
} else if (query.length > 0){
|
||||
newLocation = `/listings/${category}?q=${query}`;
|
||||
} else if (tags.length > 0) {
|
||||
newLocation = `/listings/${category}?t=${tags}`;
|
||||
} else if (category.length > 0) {
|
||||
newLocation = `/listings/${category}`;
|
||||
} else {
|
||||
newLocation = '/listings'
|
||||
}
|
||||
window.history.replaceState(null, null, newLocation);
|
||||
this.setLocation(query, tags, category, slug);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { listings, query, tags, category, allCategories, currentUserId } = this.state;
|
||||
const { listings, query, tags, category, allCategories, currentUserId, openedListing } = this.state;
|
||||
const allListings = listings.map(listing => (
|
||||
<SingleListing
|
||||
onAddTag={this.addTag}
|
||||
onChangeCategory={this.selectCategory}
|
||||
listing={listing}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
isOpen={false}
|
||||
/>
|
||||
));
|
||||
this.triggerMasonry()
|
||||
const selectedTags = tags.map(tag => (
|
||||
<span className="classified-tag">
|
||||
<a href='/listings?tags=' className='tag-name' onClick={e => this.removeTag(e, tag)} data-no-instant>
|
||||
|
|
@ -193,9 +218,27 @@ export class Listings extends Component {
|
|||
<a href={`/listings/${cat.slug}`} className={cat.slug === category ? 'selected' : ''} onClick={e => this.selectCategory(e, cat.slug)} data-no-instant>{cat.name}</a>
|
||||
))
|
||||
const clearQueryButton = query.length > 0 ? <button type="button" className='classified-search-clear' onClick={this.clearQuery}>×</button> : '';
|
||||
let modal = '';
|
||||
let modalBg = '';
|
||||
if (openedListing) {
|
||||
modalBg = <div className='classified-listings-modal-background' onClick={this.handleCloseModal} role='presentation' />
|
||||
modal = (
|
||||
<SingleListing
|
||||
onAddTag={this.addTag}
|
||||
onChangeCategory={this.selectCategory}
|
||||
listing={openedListing}
|
||||
currentUserId={currentUserId}
|
||||
onOpenModal={this.handleOpenModal}
|
||||
isOpen
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (this.state.initialFetch) {
|
||||
this.triggerMasonry();
|
||||
}
|
||||
return (
|
||||
<div className="listings__container">
|
||||
<div className="listing__overlay" id="listing-overlay" onClick={this.handleCloseModal} onKeyDown={this.handleCloseModal} role="presentation" tabIndex="0" data-origin-link="" />
|
||||
{modalBg}
|
||||
<div className="classified-filters">
|
||||
<div className="classified-filters-categories">
|
||||
<a href="/listings" className={category === '' ? 'selected' : ''} onClick={e => this.selectCategory(e, '')} data-no-instant>all</a>
|
||||
|
|
@ -211,10 +254,9 @@ export class Listings extends Component {
|
|||
<div className="classifieds-columns" id="classified-listings-results">
|
||||
{allListings}
|
||||
</div>
|
||||
{modal}
|
||||
</div>
|
||||
)
|
||||
|
||||
return <div className="github-repos">{allListings}</div>;
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +1,28 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { h } from 'preact';
|
||||
|
||||
export const SingleListing = ({listing, onAddTag, currentUserId, onChangeCategory}) => {
|
||||
export const SingleListing = ({listing, onAddTag, currentUserId, onChangeCategory, onOpenModal, isOpen}) => {
|
||||
const tagLinks = listing.tag_list.map(tag => (
|
||||
<a href={`/listings?t=${tag}`} onClick={e => onAddTag(e, tag)} data-no-instant>{tag}</a>
|
||||
));
|
||||
|
||||
const editButton = currentUserId === listing.user_id ? <a href={`/listings/${listing.id}/edit`} className="classified-listing-edit-button">・edit</a> : <a href={`/report-abuse?url=https://dev.to/listings/${listing.category}/${listing.slug}`}>・report abuse</a>;
|
||||
|
||||
const handleOpenModal = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const overlay = document.getElementById('listing-overlay')
|
||||
overlay.dataset.originLink = window.location.href
|
||||
window.history.pushState('', '', e.target.href)
|
||||
overlay.style.display = 'block'
|
||||
overlay.focus()
|
||||
overlay.tabIndex = 0
|
||||
|
||||
const { listingId } = e.target.dataset
|
||||
const clonedListing = document.getElementById(`single-classified-listing-${listingId}`).cloneNode(true)
|
||||
clonedListing.id = `single-classified-listing-modal-${listingId}`
|
||||
clonedListing.className += ' singlelisting__modal'
|
||||
overlay.insertAdjacentElement('afterend', clonedListing)
|
||||
}
|
||||
const definedClass = isOpen ? 'single-classified-listing single-classified-listing--opened' : 'single-classified-listing';
|
||||
|
||||
const listingCard = () => {
|
||||
return(
|
||||
<div className="single-classified-listing" id={`single-classified-listing-${listing.id}`}>
|
||||
<div className={definedClass} id={`single-classified-listing-${listing.id}`}>
|
||||
<div className="listing-content">
|
||||
<h3>
|
||||
<a href={`/listings/${listing.category}/${listing.slug}`} data-no-instant onClick={handleOpenModal} data-listing-id={listing.id}>
|
||||
<a href={`/listings/${listing.category}/${listing.slug}`} data-no-instant onClick={e => onOpenModal(e, listing)} data-listing-id={listing.id}>
|
||||
{listing.title}
|
||||
</a>
|
||||
</h3>
|
||||
<div className="single-classified-listing-body" dangerouslySetInnerHTML={{ __html: listing.processed_html }} />
|
||||
<div className="single-classified-listing-tags">{tagLinks}</div>
|
||||
<div className="single-classified-listing-author-info">
|
||||
<a href={`/listings/${listing.category}`} onClick={e => onChangeCategory(e, listing.category)}>{listing.category}</a>
|
||||
<a href={`/listings/${listing.category}`} onClick={e => onChangeCategory(e, listing.category)} data-no-instant>{listing.category}</a>
|
||||
・
|
||||
<a href={`/${listing.author.username}`}>{listing.author.name}</a>
|
||||
{editButton}
|
||||
|
|
@ -55,6 +40,8 @@ export const SingleListing = ({listing, onAddTag, currentUserId, onChangeCategor
|
|||
SingleListing.propTypes = {
|
||||
listing: PropTypes.object.isRequired,
|
||||
onAddTag: PropTypes.func.isRequired,
|
||||
onOpenModal: PropTypes.func.isRequired,
|
||||
onChangeCategory: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.number,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import ListingForm from '../listings/listingForm';
|
|||
|
||||
function loadElement() {
|
||||
const root = document.getElementById('listingform-data');
|
||||
const {
|
||||
listing,
|
||||
organizations,
|
||||
categoriesForSelect,
|
||||
categoriesForDetails,
|
||||
} = root.dataset;
|
||||
if (root) {
|
||||
const {
|
||||
listing,
|
||||
organizations,
|
||||
categoriesForSelect,
|
||||
categoriesForDetails,
|
||||
} = root.dataset;
|
||||
render(
|
||||
<ListingForm
|
||||
organizations={organizations}
|
||||
|
|
@ -18,6 +18,7 @@ function loadElement() {
|
|||
categoriesForDetails={categoriesForDetails}
|
||||
/>,
|
||||
root,
|
||||
root.firstElementChild
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,24 @@
|
|||
data-categories-for-select="<%= ClassifiedListing.select_options_for_categories.to_json %>"
|
||||
data-categories-for-details="<%= ClassifiedListing.categories_available.transform_values{ |value_hash| value_hash.except(:cost) }.values.to_json %>"
|
||||
>
|
||||
<div style="height: 650px;">
|
||||
<div class="field">
|
||||
<%= form.label "title" %>
|
||||
<%= form.text_field "title", placeholder: "128 characters max, plain text" %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= form.label "body_markdown", "Body Markdown" %>
|
||||
<%= form.text_area "body_markdown", placeholder: "400 characters max, 12 line break max, no images allowed" %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Category</label>
|
||||
<select></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= form.label "tags_list", "Tags" %>
|
||||
<%= form.text_field "tags_list" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= javascript_pack_tag "listingForm", defer: true %>
|
||||
<% if false # Put connect button (future feature) %>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,21 @@
|
|||
},
|
||||
) %>"
|
||||
data-allcategories="<%= ClassifiedListing.categories_for_display.to_json %>"
|
||||
<% if @displayed_classified_listing %>
|
||||
data-displayedlisting="<%= @displayed_classified_listing.to_json(
|
||||
only: %i[title processed_html tag_list category id user_id slug],
|
||||
include: {
|
||||
author: { only: %i[username name], methods: %i[username profile_image_90] },
|
||||
},
|
||||
) %> "
|
||||
<% end %>
|
||||
>
|
||||
<div class="classified-filters">
|
||||
<div class="classified-filters-categories">
|
||||
<a href="/listings" class="<%= "selected" if params[:category].blank? %> data-no-instant="true">all</a><% ClassifiedListing.categories_for_display.each do |cat| %><a href="/listings/<%= cat[:slug] %>" class="<%= "selected" if params[:category] == cat[:slug] %> data-no-instant="true"><%= cat[:name] %></a><% end %><a href='/listings/new' class='classified-create-link'>Create a Listing</a>
|
||||
<% if @displayed_classified_listing %>
|
||||
<div class="classified-listings-modal-background" role='presentation'></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<meta name="twitter:image:src" content="https://res.cloudinary.com/practicaldev/image/fetch/c_scale,h_300,w_300,r_max/u_listennow_kjml1x,y_45/<%= cloudinary(@podcast.image_url, 300) %>">
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% title "Podcasts on The Practical Dev" %>
|
||||
<% title "Podcasts" %>
|
||||
<%= content_for :page_meta do %>
|
||||
<link rel="canonical" href="https://<%= ApplicationConfig["APP_DOMAIN"] %>/pod" />
|
||||
<meta name="description" content="Discover technical podcasts for developers">
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ Rails.application.routes.draw do
|
|||
resources :buffer_updates, only: [:create]
|
||||
|
||||
get "/listings/:category" => "classified_listings#index"
|
||||
get "/listings/:category/:slug" => "classified_listings#show"
|
||||
get "/listings/:category/:slug" => "classified_listings#index"
|
||||
get "/notifications/:filter" => "notifications#index"
|
||||
get "/notifications/:filter/:org_id" => "notifications#index"
|
||||
patch "/onboarding_update" => "users#onboarding_update"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue