Update JS listings form and other tweaks (#2697)

This commit is contained in:
Ben Halpern 2019-05-05 10:14:07 -04:00 committed by GitHub
parent 04758f46f8
commit 9e7dbf8356
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 113 deletions

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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>
)

View file

@ -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>;
)
}
}

View file

@ -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,
};

View file

@ -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
);
}
}

View file

@ -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) %>

View file

@ -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>

View file

@ -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">

View file

@ -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"